main commit
This commit is contained in:
parent
436ccb2e53
commit
0506748dee
1
sk1
1
sk1
@ -1 +0,0 @@
|
||||
Subproject commit 4a6b3a95ecefc48c2f0be168e3a76be45bb02c78
|
||||
55
sk1/.github/workflows/deploy-summarizer.yml
vendored
Normal file
55
sk1/.github/workflows/deploy-summarizer.yml
vendored
Normal file
@ -0,0 +1,55 @@
|
||||
name: Deploy Summarizer
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'summarizer/**'
|
||||
- '.github/workflows/deploy-summarizer.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE: ghcr.io/${{ github.repository_owner }}/readitlater-summarizer
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: summarizer
|
||||
push: true
|
||||
tags: ${{ env.IMAGE }}:latest,${{ env.IMAGE }}:${{ github.sha }}
|
||||
|
||||
- name: Deploy to instance
|
||||
uses: appleboy/ssh-action@v1
|
||||
with:
|
||||
host: ${{ secrets.SUMMARIZER_HOST }}
|
||||
username: ec2-user
|
||||
key: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
proxy_host: ${{ secrets.WEB_HOST }}
|
||||
proxy_username: ec2-user
|
||||
proxy_key: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
script: |
|
||||
echo '${{ secrets.GITHUB_TOKEN }}' | sudo docker login ghcr.io -u ${{ github.actor }} --password-stdin
|
||||
sudo docker pull ${{ env.IMAGE }}:latest
|
||||
sudo docker stop summarizer || true
|
||||
sudo docker rm summarizer || true
|
||||
sudo docker run -d \
|
||||
--name summarizer \
|
||||
--restart always \
|
||||
-p 8000:8000 \
|
||||
${{ env.IMAGE }}:latest
|
||||
81
sk1/.github/workflows/deploy-web.yml
vendored
Normal file
81
sk1/.github/workflows/deploy-web.yml
vendored
Normal file
@ -0,0 +1,81 @@
|
||||
name: Deploy Web
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'web/**'
|
||||
- '.github/workflows/deploy-web.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
NGINX_IMAGE: ghcr.io/${{ github.repository_owner }}/readitlater-nginx
|
||||
API_IMAGE: ghcr.io/${{ github.repository_owner }}/readitlater-api
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: web/nginx
|
||||
push: true
|
||||
tags: ${{ env.NGINX_IMAGE }}:latest,${{ env.NGINX_IMAGE }}:${{ github.sha }}
|
||||
|
||||
- uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: web/api
|
||||
push: true
|
||||
tags: ${{ env.API_IMAGE }}:latest,${{ env.API_IMAGE }}:${{ github.sha }}
|
||||
|
||||
- name: Deploy to instance
|
||||
uses: appleboy/ssh-action@v1
|
||||
with:
|
||||
host: ${{ secrets.WEB_HOST }}
|
||||
username: ec2-user
|
||||
key: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
script: |
|
||||
sudo chown -R ec2-user:ec2-user /opt/app
|
||||
echo "GITHUB_OWNER=${{ github.repository_owner }}" > /opt/app/.env
|
||||
echo "DB_HOST=${{ secrets.DB_HOST }}" >> /opt/app/.env
|
||||
echo "DB_PORT=5432" >> /opt/app/.env
|
||||
echo "DB_NAME=${{ secrets.DB_NAME }}" >> /opt/app/.env
|
||||
echo "DB_USER=${{ secrets.DB_USER }}" >> /opt/app/.env
|
||||
echo "DB_PASSWORD=${{ secrets.DB_PASSWORD }}" >> /opt/app/.env
|
||||
echo "SUMMARIZER_URL=http://${{ secrets.SUMMARIZER_HOST }}:8000" >> /opt/app/.env
|
||||
echo '${{ secrets.GITHUB_TOKEN }}' | sudo docker login ghcr.io -u ${{ github.actor }} --password-stdin
|
||||
sudo docker pull ${{ env.NGINX_IMAGE }}:latest
|
||||
sudo docker pull ${{ env.API_IMAGE }}:latest
|
||||
cd /opt/app
|
||||
cat > docker-compose.yml << 'COMPOSE'
|
||||
services:
|
||||
nginx:
|
||||
image: ghcr.io/${{ github.repository_owner }}/readitlater-nginx:latest
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
environment:
|
||||
- CERTBOT_EMAIL=pavelyman76@gmail.com
|
||||
depends_on:
|
||||
- api
|
||||
restart: always
|
||||
api:
|
||||
image: ghcr.io/${{ github.repository_owner }}/readitlater-api:latest
|
||||
env_file: .env
|
||||
restart: always
|
||||
COMPOSE
|
||||
sudo /usr/local/bin/docker-compose down || true
|
||||
sudo /usr/local/bin/docker-compose up -d
|
||||
6
sk1/.gitignore
vendored
Normal file
6
sk1/.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
terraform/terraform*
|
||||
terraform/.terraform*
|
||||
terraform/security_groups.tf
|
||||
*.tfstate
|
||||
*.tfstate.*
|
||||
.terraform/
|
||||
72
sk1/README.md
Normal file
72
sk1/README.md
Normal file
@ -0,0 +1,72 @@
|
||||
# Read-it-later AI Summarizer
|
||||
|
||||
## 1. Opis aplikácie
|
||||
**Read-it-later** je moderná mikroslužbová webová aplikácia, ktorá slúži ako inteligentný archív článkov. Používateľ vloží URL adresu dlhého článku, aplikácia ho automaticky stiahne a pomocou modelu umelej inteligencie (AI) vygeneruje jeho stručné zhrnutie. Výsledok si používateľ môže neskôr kedykoľvek prečítať v responzívnom webovom rozhraní.
|
||||
|
||||
## 2. Architektúra a využité Cloud technológie
|
||||
Aplikácia je nasadená v prostredí verejného cloudu **AWS (Amazon Web Services)** a využíva plne automatizovaný prístup Infrastructure as Code (IaC) cez Terraform.
|
||||
* **Virtual Private Cloud (VPC):** Izolovaná sieť s nakonfigurovanými podsúbormi a bezpečnostnými skupinami pre riadenie sieťovej prevádzky.
|
||||
* **EC2 (Elastic Compute Cloud) & Docker:** Dva dedikované virtuálne servery bežiace na OS Amazon Linux 2023. Jeden zabezpečuje web (Nginx a FastAPI) a druhý slúži pre výpočetne náročnejší AI model. Samotné nasadenie beží vo vnútri **Docker** kontajnerov orchestráciou cez Docker Compose.
|
||||
* **Amazon RDS pre PostgreSQL:** Plne spravovaná relačná databáza, do ktorej sa ukladajú používateľské záznamy, originálne texty a AI zhrnutia. Trvalé zväzky (Persistent Storage - EBS a RDS Storage) sú šifrované a zabezpečujú trvalé uchovanie dát aj po reštarte služieb.
|
||||
* **AWS Route 53:** Služba DNS, ktorá automaticky spravuje doménové záznamy a prekladá doménu na verejnú IP adresu (Elastic IP) hlavného servera.
|
||||
* **SSL a HTTPS:** Smerovanie z verejného internetu je zabezpečené certifikátmi od Let's Encrypt, pričom Nginx slúži ako reverzný proxy server (`jonasal/nginx-certbot`).
|
||||
|
||||
## 3. Analýza nákladov (1 rok)
|
||||
Odhadovaný prevádzkový profil: **1000 používateľov za deň** a maximálna veľkosť databázy **50 GB**.
|
||||
Keďže prevádzkujeme model umelej inteligencie, je vyžadovaný aspoň jeden výkonnejší stroj (`t3.medium`). Kalkulácia nezahŕňa doménový poplatok u registrátora tretej strany.
|
||||
|
||||
| Služba AWS | Typ / Špecifikácia | Fakturačný interval | Cena za mesiac | Cena za 1 rok |
|
||||
| :--- | :--- | :--- | :--- | :--- |
|
||||
| **EC2 (Web Server)** | `t3.micro` (2 vCPU, 1 GiB RAM) + 30GB EBS gp3 | Hodinový | ~ $7.60 | ~ $91.20 |
|
||||
| **EC2 (AI Server)** | `t3.medium` (2 vCPU, 4 GiB RAM) + 30GB EBS gp3 | Hodinový | ~ $30.40 | ~ $364.80 |
|
||||
| **Amazon RDS** | `db.t4g.micro` (PostgreSQL) | Hodinový | ~ $11.90 | ~ $142.80 |
|
||||
| **RDS Storage** | 50 GB `gp2` zväzok pre databázu | Mesačný GB | ~ $5.75 | ~ $69.00 |
|
||||
| **Route 53** | 1 Hosted Zone + DNS dopyty | Mesačný | ~ $0.50 | ~ $6.00 |
|
||||
| **Sieť (Data Transfer)** | Odhadovaný odchádzajúci prenos dát | Za GB | ~ $2.00 | ~ $24.00 |
|
||||
| **Spolu (Odhad)** | | | **~ $58.15** | **~ $697.80** |
|
||||
|
||||
## 4. Opis odovzdaných súborov
|
||||
* `prepare-app.sh`: Skript pre plne automatické vytvorenie klaudového prostredia.
|
||||
* `remove-app.sh`: Skript pre deštrukciu všetkých cloudových služieb.
|
||||
* `terraform/`: Priečinok obsahujúci kompletnú deklaratívnu definíciu AWS infraštruktúry (IaC). Obsahuje `.tf` súbory pre siete, servery, databázu a bezpečnosť.
|
||||
* `web/`: Zdrojové kódy webovej časti (HTML/CSS), backendu (FastAPI v Pythone), konfiguračný súbor pre Nginx a Docker prostredie.
|
||||
* `summarizer/`: Kód PyTorch AI mikroslužby vrátane `Dockerfile` a `requirements.txt` pre stiahnutie modelu BART.
|
||||
* `.github/workflows/`: Skripty pre automatické CI/CD (kontinuálna integrácia a doručovanie), ktoré aplikáciu zostavia a nasadia po každej zmene v zdrojovom kóde.
|
||||
|
||||
## 5. Konfigurácia (Terraform & CI/CD)
|
||||
Infraštruktúra je definovaná formou kódu cez Terraform. Terraform konfigurácia (`ec2.tf`, `vpc.tf`, `rds.tf`) automaticky prepojuje sieťové rozhrania, nastavuje pravidlá Firewallu (Security Groups) a vytvorí kľúče pre SSH prístup.
|
||||
Softvérové nasadenie zabezpečujú bez-agentové GitHub Actions. Aplikácie sa po commite zabalia do Docker imidžov, uložia sa do GitHub Container Registry (GHCR) a následne sa prostredníctvom SSH tunelu automaticky stiahnu a spustia cez `docker-compose` na cieľových virtuálnych strojoch v AWS.
|
||||
|
||||
## 6. Návod na použitie
|
||||
1. Otvorte ľubovoľný webový prehliadač a prejdite na adresu URL poskytnutú po inštalácii (napríklad `https://devopspavel.me`).
|
||||
2. Do vstupného poľa vložte odkaz na dlhý internetový článok, ktorý si prajete zosumarizovať.
|
||||
3. Kliknite na tlačidlo **Save**.
|
||||
4. Systém odošle požiadavku do umelej inteligencie a o niekoľko sekúnd zobrazí originálny link spoločne so skráteným AI textom.
|
||||
|
||||
## 7. Záloha dát (Backup)
|
||||
Záloha používateľských dát je spravovaná automaticky priamo na úrovni Amazon RDS (Automated Backups). Databáza má predvolene nastavené 7-dňové retenčné obdobie, počas ktorého AWS vykonáva denné kópie (snapshots).
|
||||
V prípade potreby manuálnej zálohy (Full Backup) do súboru je nutné:
|
||||
1. Pripojiť sa cez SSH na Web Server inštanciu (`t3.micro`), ktorá má prístup do privátnej siete RDS.
|
||||
2. Vykonať príkaz: `pg_dump -h <adresa_rds> -U appuser -d aisummarizer -F c -f zaloha.dump`.
|
||||
|
||||
## 8. Záznamy o prístupoch z internetu
|
||||
Logy o pripojeniach používateľov je možné sledovať priamo z Nginx kontajnera. Pre zobrazenie najnovších prístupov z internetu:
|
||||
1. Pripojte sa na Web Server prostredníctvom priloženého SSH kľúča.
|
||||
2. Pre zobrazenie "live" prístupov spustite: `sudo docker logs -f nginx`
|
||||
Tieto logy zaznamenávajú zdrojovú IP adresu, prehliadač a typ dopytu (GET/POST).
|
||||
|
||||
## 9. Podmienky spustenia skriptov
|
||||
Pre úspešné spustenie skriptov `prepare-app.sh` a `remove-app.sh` je nutné:
|
||||
1. Mať v operačnom systéme nainštalovaný nástroj **Terraform** (verzia >= 1.0).
|
||||
2. Mať nainštalovaný balík **AWS CLI**.
|
||||
3. Mať v systéme nakonfigurované platné prístupové kľúče `AWS_ACCESS_KEY_ID` a `AWS_SECRET_ACCESS_KEY` do prostredia AWS.
|
||||
4. Používať Unix/Linux/macOS terminál pre bezproblémové spustenie `.sh` súborov.
|
||||
|
||||
## 10. Zoznam externých zdrojov a využitie Generatívnej AI
|
||||
Pri návrhu architektúry, vývoji aplikácie a tvorbe infraštruktúry bol využitý generatívny model umelej inteligencie **Antigravity (Google DeepMind)**.
|
||||
Model bol použitý ako inteligentný agent (Pair-programming) predovšetkým na:
|
||||
- Tvorbu a optimalizáciu **Python skriptov** (FastAPI backend a PyTorch AI Summarizer).
|
||||
- Pripravenie deklaratívnych `.tf` konfigurácií a GitHub Actions Pipelines.
|
||||
- Štruktúrovanie a formátovanie tejto dokumentácie v slovenskom jazyku.
|
||||
|
||||
Aplikácia interne využíva Open-Source zmenšený model `facebook/bart-large-cnn` zverejnený na platforme HuggingFace pre úlohy sumarizácie textu.
|
||||
14
sk1/prepare-app.sh
Executable file
14
sk1/prepare-app.sh
Executable file
@ -0,0 +1,14 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "Začínam prípravu infraštruktúry pre aplikáciu Read-it-later..."
|
||||
|
||||
cd terraform || exit 1
|
||||
|
||||
echo "Inicializujem Terraform prostredie..."
|
||||
terraform init
|
||||
|
||||
echo "Vytváram a nasadzujem všetky služby (EC2, RDS, Sieť)..."
|
||||
terraform apply -auto-approve
|
||||
|
||||
echo "Infraštruktúra je úspešne nasadená!"
|
||||
echo "Nezabudnite skopírovať vygenerované tajné kľúče (secrets) do GitHub Actions pre automatický deploing."
|
||||
13
sk1/remove-app.sh
Executable file
13
sk1/remove-app.sh
Executable file
@ -0,0 +1,13 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "Varovanie: Tento skript natrvalo vymaže celú infraštruktúru aplikácie vrátane databázy!"
|
||||
read -p "Ste si istý, že chcete pokračovať? (y/N): " confirm
|
||||
|
||||
if [[ $confirm == [yY] || $confirm == [yY][eE][sS] ]]; then
|
||||
cd terraform || exit 1
|
||||
echo "Ruším všetky cloudové služby..."
|
||||
terraform destroy -auto-approve
|
||||
echo "Aplikácia a cloudové zdroje boli úspešne odstránené."
|
||||
else
|
||||
echo "Operácia zrušená."
|
||||
fi
|
||||
7
sk1/summarizer/Dockerfile
Normal file
7
sk1/summarizer/Dockerfile
Normal file
@ -0,0 +1,7 @@
|
||||
FROM python:3.11-slim
|
||||
WORKDIR /app
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
COPY main.py .
|
||||
RUN python -c "from transformers import AutoTokenizer, AutoModelForSeq2SeqLM; AutoTokenizer.from_pretrained('facebook/bart-large-cnn'); AutoModelForSeq2SeqLM.from_pretrained('facebook/bart-large-cnn')"
|
||||
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
37
sk1/summarizer/main.py
Normal file
37
sk1/summarizer/main.py
Normal file
@ -0,0 +1,37 @@
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel
|
||||
from transformers import AutoTokenizer, AutoModelForSeq2SeqLM
|
||||
import torch
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
tokenizer = AutoTokenizer.from_pretrained("facebook/bart-large-cnn")
|
||||
model = AutoModelForSeq2SeqLM.from_pretrained("facebook/bart-large-cnn")
|
||||
|
||||
|
||||
class TextRequest(BaseModel):
|
||||
text: str
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@app.post("/summarize")
|
||||
def summarize(req: TextRequest):
|
||||
inputs = tokenizer(
|
||||
req.text,
|
||||
max_length=1024,
|
||||
truncation=True,
|
||||
return_tensors="pt",
|
||||
)
|
||||
summary_ids = model.generate(
|
||||
**inputs,
|
||||
max_length=150,
|
||||
min_length=40,
|
||||
num_beams=4,
|
||||
length_penalty=2.0,
|
||||
)
|
||||
summary = tokenizer.decode(summary_ids[0], skip_special_tokens=True)
|
||||
return {"summary": summary}
|
||||
4
sk1/summarizer/requirements.txt
Normal file
4
sk1/summarizer/requirements.txt
Normal file
@ -0,0 +1,4 @@
|
||||
fastapi==0.115.0
|
||||
uvicorn==0.30.0
|
||||
transformers==4.44.0
|
||||
torch==2.4.0
|
||||
9
sk1/terraform/cloudwatch.tf
Normal file
9
sk1/terraform/cloudwatch.tf
Normal file
@ -0,0 +1,9 @@
|
||||
resource "aws_cloudwatch_log_group" "nginx" {
|
||||
name = "/ec2/${local.name_prefix}/nginx"
|
||||
retention_in_days = var.log_retention_days
|
||||
}
|
||||
|
||||
resource "aws_cloudwatch_log_group" "app" {
|
||||
name = "/ec2/${local.name_prefix}/app"
|
||||
retention_in_days = var.log_retention_days
|
||||
}
|
||||
20
sk1/terraform/data.tf
Normal file
20
sk1/terraform/data.tf
Normal file
@ -0,0 +1,20 @@
|
||||
data "aws_availability_zones" "available" {
|
||||
state = "available"
|
||||
}
|
||||
|
||||
data "aws_caller_identity" "current" {}
|
||||
|
||||
data "aws_ami" "amazon_linux" {
|
||||
most_recent = true
|
||||
owners = ["amazon"]
|
||||
|
||||
filter {
|
||||
name = "name"
|
||||
values = ["al2023-ami-*-x86_64"]
|
||||
}
|
||||
|
||||
filter {
|
||||
name = "virtualization-type"
|
||||
values = ["hvm"]
|
||||
}
|
||||
}
|
||||
82
sk1/terraform/ec2.tf
Normal file
82
sk1/terraform/ec2.tf
Normal file
@ -0,0 +1,82 @@
|
||||
resource "tls_private_key" "main" {
|
||||
algorithm = "RSA"
|
||||
rsa_bits = 4096
|
||||
}
|
||||
|
||||
resource "aws_key_pair" "main" {
|
||||
key_name = "${local.name_prefix}-key"
|
||||
public_key = tls_private_key.main.public_key_openssh
|
||||
}
|
||||
|
||||
resource "aws_eip" "nginx" {
|
||||
domain = "vpc"
|
||||
|
||||
tags = {
|
||||
Name = "${local.name_prefix}-nginx-eip"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_eip_association" "nginx" {
|
||||
instance_id = aws_instance.nginx.id
|
||||
allocation_id = aws_eip.nginx.id
|
||||
}
|
||||
|
||||
resource "aws_instance" "nginx" {
|
||||
ami = data.aws_ami.amazon_linux.id
|
||||
instance_type = var.nginx_instance_type
|
||||
subnet_id = aws_subnet.public[0].id
|
||||
vpc_security_group_ids = [aws_security_group.nginx.id]
|
||||
key_name = aws_key_pair.main.key_name
|
||||
iam_instance_profile = aws_iam_instance_profile.ec2.name
|
||||
|
||||
root_block_device {
|
||||
volume_size = 30
|
||||
volume_type = "gp3"
|
||||
encrypted = true
|
||||
}
|
||||
|
||||
user_data = <<-EOF
|
||||
#!/bin/bash
|
||||
set -e
|
||||
dnf update -y
|
||||
dnf install -y docker
|
||||
systemctl enable docker
|
||||
systemctl start docker
|
||||
curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
||||
chmod +x /usr/local/bin/docker-compose
|
||||
mkdir -p /opt/app
|
||||
EOF
|
||||
|
||||
tags = {
|
||||
Name = "${local.name_prefix}-nginx"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_instance" "app" {
|
||||
ami = data.aws_ami.amazon_linux.id
|
||||
instance_type = var.app_instance_type
|
||||
subnet_id = aws_subnet.public[0].id
|
||||
vpc_security_group_ids = [aws_security_group.app.id]
|
||||
key_name = aws_key_pair.main.key_name
|
||||
iam_instance_profile = aws_iam_instance_profile.ec2.name
|
||||
|
||||
root_block_device {
|
||||
volume_size = 30
|
||||
volume_type = "gp3"
|
||||
encrypted = true
|
||||
}
|
||||
|
||||
user_data = <<-EOF
|
||||
#!/bin/bash
|
||||
set -e
|
||||
dnf update -y
|
||||
dnf install -y docker aws-cli
|
||||
systemctl enable docker
|
||||
systemctl start docker
|
||||
mkdir -p /opt/app
|
||||
EOF
|
||||
|
||||
tags = {
|
||||
Name = "${local.name_prefix}-app"
|
||||
}
|
||||
}
|
||||
60
sk1/terraform/iam.tf
Normal file
60
sk1/terraform/iam.tf
Normal file
@ -0,0 +1,60 @@
|
||||
resource "aws_iam_role" "ec2" {
|
||||
name = "${local.name_prefix}-ec2-role"
|
||||
|
||||
assume_role_policy = jsonencode({
|
||||
Version = "2012-10-17"
|
||||
Statement = [
|
||||
{
|
||||
Action = "sts:AssumeRole"
|
||||
Effect = "Allow"
|
||||
Principal = {
|
||||
Service = "ec2.amazonaws.com"
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
resource "aws_iam_role_policy_attachment" "ssm_core" {
|
||||
role = aws_iam_role.ec2.name
|
||||
policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
|
||||
}
|
||||
|
||||
resource "aws_iam_policy" "ec2_app" {
|
||||
name = "${local.name_prefix}-ec2-app"
|
||||
|
||||
policy = jsonencode({
|
||||
Version = "2012-10-17"
|
||||
Statement = [
|
||||
{
|
||||
Effect = "Allow"
|
||||
Action = [
|
||||
"ssm:GetParameter",
|
||||
"ssm:GetParameters"
|
||||
]
|
||||
Resource = [
|
||||
aws_ssm_parameter.db_password.arn
|
||||
]
|
||||
},
|
||||
{
|
||||
Effect = "Allow"
|
||||
Action = [
|
||||
"logs:CreateLogStream",
|
||||
"logs:PutLogEvents",
|
||||
"logs:DescribeLogStreams"
|
||||
]
|
||||
Resource = "arn:aws:logs:${var.aws_region}:${data.aws_caller_identity.current.account_id}:log-group:/ec2/${local.name_prefix}*"
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
resource "aws_iam_role_policy_attachment" "ec2_app" {
|
||||
role = aws_iam_role.ec2.name
|
||||
policy_arn = aws_iam_policy.ec2_app.arn
|
||||
}
|
||||
|
||||
resource "aws_iam_instance_profile" "ec2" {
|
||||
name = "${local.name_prefix}-ec2-profile"
|
||||
role = aws_iam_role.ec2.name
|
||||
}
|
||||
9
sk1/terraform/locals.tf
Normal file
9
sk1/terraform/locals.tf
Normal file
@ -0,0 +1,9 @@
|
||||
locals {
|
||||
common_tags = {
|
||||
Project = var.project_name
|
||||
Environment = var.environment
|
||||
ManagedBy = "terraform"
|
||||
}
|
||||
|
||||
name_prefix = "${var.project_name}-${var.environment}"
|
||||
}
|
||||
49
sk1/terraform/outputs.tf
Normal file
49
sk1/terraform/outputs.tf
Normal file
@ -0,0 +1,49 @@
|
||||
output "vpc_id" {
|
||||
value = aws_vpc.main.id
|
||||
}
|
||||
|
||||
output "nginx_public_ip" {
|
||||
value = aws_eip.nginx.public_ip
|
||||
}
|
||||
|
||||
output "nginx_instance_id" {
|
||||
value = aws_instance.nginx.id
|
||||
}
|
||||
|
||||
output "app_instance_id" {
|
||||
value = aws_instance.app.id
|
||||
}
|
||||
|
||||
output "app_private_ip" {
|
||||
value = aws_instance.app.private_ip
|
||||
}
|
||||
|
||||
output "app_url" {
|
||||
value = "https://${var.domain_name}"
|
||||
}
|
||||
|
||||
output "rds_endpoint" {
|
||||
value = aws_db_instance.main.endpoint
|
||||
}
|
||||
|
||||
output "rds_db_name" {
|
||||
value = aws_db_instance.main.db_name
|
||||
}
|
||||
|
||||
output "rds_db_user" {
|
||||
value = aws_db_instance.main.username
|
||||
}
|
||||
|
||||
output "rds_db_password" {
|
||||
value = random_password.db.result
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
output "name_servers" {
|
||||
value = aws_route53_zone.main.name_servers
|
||||
}
|
||||
|
||||
output "ssh_private_key" {
|
||||
value = tls_private_key.main.private_key_pem
|
||||
sensitive = true
|
||||
}
|
||||
28
sk1/terraform/providers.tf
Normal file
28
sk1/terraform/providers.tf
Normal file
@ -0,0 +1,28 @@
|
||||
terraform {
|
||||
required_version = ">= 1.5.0"
|
||||
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = "~> 5.0"
|
||||
}
|
||||
random = {
|
||||
source = "hashicorp/random"
|
||||
version = "~> 3.5"
|
||||
}
|
||||
tls = {
|
||||
source = "hashicorp/tls"
|
||||
version = "~> 4.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
provider "aws" {
|
||||
region = var.aws_region
|
||||
access_key = var.terraform_aws_access_key
|
||||
secret_key = var.terraform_aws_secret_key
|
||||
|
||||
default_tags {
|
||||
tags = local.common_tags
|
||||
}
|
||||
}
|
||||
54
sk1/terraform/rds.tf
Normal file
54
sk1/terraform/rds.tf
Normal file
@ -0,0 +1,54 @@
|
||||
resource "random_password" "db" {
|
||||
length = 24
|
||||
special = false
|
||||
}
|
||||
|
||||
resource "aws_ssm_parameter" "db_password" {
|
||||
name = "/${var.project_name}/${var.environment}/db-password"
|
||||
type = "SecureString"
|
||||
value = random_password.db.result
|
||||
}
|
||||
|
||||
resource "aws_db_subnet_group" "main" {
|
||||
name = "${local.name_prefix}-db-subnet"
|
||||
subnet_ids = aws_subnet.private[*].id
|
||||
|
||||
tags = {
|
||||
Name = "${local.name_prefix}-db-subnet"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_db_instance" "main" {
|
||||
identifier = "${local.name_prefix}-db"
|
||||
|
||||
engine = "postgres"
|
||||
engine_version = var.db_engine_version
|
||||
instance_class = var.db_instance_class
|
||||
|
||||
allocated_storage = var.db_allocated_storage
|
||||
storage_type = "gp2"
|
||||
storage_encrypted = true
|
||||
|
||||
db_name = var.db_name
|
||||
username = var.db_username
|
||||
password = random_password.db.result
|
||||
|
||||
db_subnet_group_name = aws_db_subnet_group.main.name
|
||||
vpc_security_group_ids = [aws_security_group.rds.id]
|
||||
|
||||
multi_az = false
|
||||
publicly_accessible = false
|
||||
|
||||
backup_retention_period = 7
|
||||
backup_window = "03:00-04:00"
|
||||
maintenance_window = "mon:04:00-mon:05:00"
|
||||
|
||||
skip_final_snapshot = true
|
||||
final_snapshot_identifier = "${local.name_prefix}-db-final"
|
||||
|
||||
deletion_protection = false
|
||||
|
||||
tags = {
|
||||
Name = "${local.name_prefix}-db"
|
||||
}
|
||||
}
|
||||
11
sk1/terraform/route53.tf
Normal file
11
sk1/terraform/route53.tf
Normal file
@ -0,0 +1,11 @@
|
||||
resource "aws_route53_zone" "main" {
|
||||
name = var.domain_name
|
||||
}
|
||||
|
||||
resource "aws_route53_record" "app" {
|
||||
zone_id = aws_route53_zone.main.zone_id
|
||||
name = var.domain_name
|
||||
type = "A"
|
||||
ttl = 300
|
||||
records = [aws_eip.nginx.public_ip]
|
||||
}
|
||||
83
sk1/terraform/variables.tf
Normal file
83
sk1/terraform/variables.tf
Normal file
@ -0,0 +1,83 @@
|
||||
variable "project_name" {
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "environment" {
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "aws_region" {
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "vpc_cidr" {
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "public_subnet_cidrs" {
|
||||
type = list(string)
|
||||
}
|
||||
|
||||
variable "private_subnet_cidrs" {
|
||||
type = list(string)
|
||||
}
|
||||
|
||||
variable "domain_name" {
|
||||
type = string
|
||||
}
|
||||
|
||||
|
||||
|
||||
variable "db_name" {
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "db_username" {
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "db_instance_class" {
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "db_allocated_storage" {
|
||||
type = number
|
||||
}
|
||||
|
||||
variable "db_engine_version" {
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "nginx_instance_type" {
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "app_instance_type" {
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "app_port" {
|
||||
type = number
|
||||
}
|
||||
|
||||
|
||||
variable "allowed_ssh_cidr" {
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "log_retention_days" {
|
||||
type = number
|
||||
}
|
||||
|
||||
variable "terraform_aws_access_key" {
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "terraform_aws_secret_key" {
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
variable "admin_email" {
|
||||
type = string
|
||||
}
|
||||
80
sk1/terraform/vpc.tf
Normal file
80
sk1/terraform/vpc.tf
Normal file
@ -0,0 +1,80 @@
|
||||
resource "aws_vpc" "main" {
|
||||
cidr_block = var.vpc_cidr
|
||||
enable_dns_support = true
|
||||
enable_dns_hostnames = true
|
||||
|
||||
tags = {
|
||||
Name = "${local.name_prefix}-vpc"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_internet_gateway" "main" {
|
||||
vpc_id = aws_vpc.main.id
|
||||
|
||||
tags = {
|
||||
Name = "${local.name_prefix}-igw"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_subnet" "public" {
|
||||
count = length(var.public_subnet_cidrs)
|
||||
|
||||
vpc_id = aws_vpc.main.id
|
||||
cidr_block = var.public_subnet_cidrs[count.index]
|
||||
availability_zone = data.aws_availability_zones.available.names[count.index]
|
||||
map_public_ip_on_launch = true
|
||||
|
||||
tags = {
|
||||
Name = "${local.name_prefix}-public-${count.index + 1}"
|
||||
Tier = "public"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_subnet" "private" {
|
||||
count = length(var.private_subnet_cidrs)
|
||||
|
||||
vpc_id = aws_vpc.main.id
|
||||
cidr_block = var.private_subnet_cidrs[count.index]
|
||||
availability_zone = data.aws_availability_zones.available.names[count.index]
|
||||
|
||||
tags = {
|
||||
Name = "${local.name_prefix}-private-${count.index + 1}"
|
||||
Tier = "private"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
resource "aws_route_table" "public" {
|
||||
vpc_id = aws_vpc.main.id
|
||||
|
||||
route {
|
||||
cidr_block = "0.0.0.0/0"
|
||||
gateway_id = aws_internet_gateway.main.id
|
||||
}
|
||||
|
||||
tags = {
|
||||
Name = "${local.name_prefix}-public-rt"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_route_table" "private" {
|
||||
vpc_id = aws_vpc.main.id
|
||||
|
||||
tags = {
|
||||
Name = "${local.name_prefix}-private-rt"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_route_table_association" "public" {
|
||||
count = length(aws_subnet.public)
|
||||
|
||||
subnet_id = aws_subnet.public[count.index].id
|
||||
route_table_id = aws_route_table.public.id
|
||||
}
|
||||
|
||||
resource "aws_route_table_association" "private" {
|
||||
count = length(aws_subnet.private)
|
||||
|
||||
subnet_id = aws_subnet.private[count.index].id
|
||||
route_table_id = aws_route_table.private.id
|
||||
}
|
||||
6
sk1/web/api/Dockerfile
Normal file
6
sk1/web/api/Dockerfile
Normal file
@ -0,0 +1,6 @@
|
||||
FROM python:3.11-slim
|
||||
WORKDIR /app
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
COPY main.py .
|
||||
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
89
sk1/web/api/main.py
Normal file
89
sk1/web/api/main.py
Normal file
@ -0,0 +1,89 @@
|
||||
import os
|
||||
import httpx
|
||||
import trafilatura
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from pydantic import BaseModel
|
||||
import asyncpg
|
||||
import asyncio
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
DB_DSN = "postgresql://{user}:{password}@{host}:{port}/{dbname}".format(
|
||||
user=os.environ["DB_USER"],
|
||||
password=os.environ["DB_PASSWORD"],
|
||||
host=os.environ["DB_HOST"],
|
||||
port=os.environ.get("DB_PORT", "5432"),
|
||||
dbname=os.environ["DB_NAME"],
|
||||
)
|
||||
SUMMARIZER_URL = os.environ.get("SUMMARIZER_URL", "http://summarizer:8000")
|
||||
|
||||
pool = None
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
global pool
|
||||
pool = await asyncpg.create_pool(DB_DSN)
|
||||
await pool.execute("""
|
||||
CREATE TABLE IF NOT EXISTS articles (
|
||||
id SERIAL PRIMARY KEY,
|
||||
url TEXT NOT NULL,
|
||||
title TEXT DEFAULT '',
|
||||
full_text TEXT DEFAULT '',
|
||||
summary TEXT DEFAULT '',
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
)
|
||||
""")
|
||||
yield
|
||||
await pool.close()
|
||||
|
||||
|
||||
app = FastAPI(lifespan=lifespan)
|
||||
|
||||
|
||||
class URLRequest(BaseModel):
|
||||
url: str
|
||||
|
||||
|
||||
@app.get("/api/health")
|
||||
async def health():
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@app.get("/api/articles")
|
||||
async def list_articles():
|
||||
rows = await pool.fetch(
|
||||
"SELECT id, url, title, summary, created_at "
|
||||
"FROM articles ORDER BY created_at DESC LIMIT 50"
|
||||
)
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
|
||||
|
||||
@app.post("/api/articles")
|
||||
async def create_article(req: URLRequest):
|
||||
downloaded = await asyncio.to_thread(trafilatura.fetch_url, req.url)
|
||||
if not downloaded:
|
||||
raise HTTPException(400, "Failed to fetch URL")
|
||||
|
||||
result = await asyncio.to_thread(trafilatura.bare_extraction, downloaded, include_comments=False)
|
||||
if not result or not result.get("text"):
|
||||
raise HTTPException(400, "Failed to extract text")
|
||||
|
||||
text = result["text"]
|
||||
title = result.get("title", "")
|
||||
|
||||
async with httpx.AsyncClient(timeout=120.0) as client:
|
||||
resp = await client.post(
|
||||
f"{SUMMARIZER_URL}/summarize", json={"text": text}
|
||||
)
|
||||
if resp.status_code != 200:
|
||||
raise HTTPException(502, "Summarizer service unavailable")
|
||||
summary = resp.json()["summary"]
|
||||
|
||||
row = await pool.fetchrow(
|
||||
"INSERT INTO articles (url, title, full_text, summary) "
|
||||
"VALUES ($1, $2, $3, $4) "
|
||||
"RETURNING id, url, title, summary, created_at",
|
||||
req.url, title, text, summary,
|
||||
)
|
||||
return dict(row)
|
||||
5
sk1/web/api/requirements.txt
Normal file
5
sk1/web/api/requirements.txt
Normal file
@ -0,0 +1,5 @@
|
||||
fastapi==0.115.0
|
||||
uvicorn==0.30.0
|
||||
asyncpg==0.29.0
|
||||
httpx==0.27.0
|
||||
trafilatura==1.12.0
|
||||
13
sk1/web/docker-compose.yml
Normal file
13
sk1/web/docker-compose.yml
Normal file
@ -0,0 +1,13 @@
|
||||
services:
|
||||
nginx:
|
||||
image: ghcr.io/${GITHUB_OWNER}/readitlater-nginx:latest
|
||||
ports:
|
||||
- "80:80"
|
||||
depends_on:
|
||||
- api
|
||||
restart: always
|
||||
|
||||
api:
|
||||
image: ghcr.io/${GITHUB_OWNER}/readitlater-api:latest
|
||||
env_file: .env
|
||||
restart: always
|
||||
3
sk1/web/nginx/Dockerfile
Normal file
3
sk1/web/nginx/Dockerfile
Normal file
@ -0,0 +1,3 @@
|
||||
FROM jonasal/nginx-certbot:latest
|
||||
COPY nginx.conf /etc/nginx/user_conf.d/default.conf
|
||||
COPY html/ /usr/share/nginx/html/
|
||||
92
sk1/web/nginx/html/index.html
Normal file
92
sk1/web/nginx/html/index.html
Normal file
@ -0,0 +1,92 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Read it later</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
||||
background: #fff;
|
||||
color: #1a1a1a;
|
||||
max-width: 640px;
|
||||
margin: 0 auto;
|
||||
padding: 48px 24px;
|
||||
}
|
||||
h1 { font-size: 20px; font-weight: 600; margin-bottom: 32px; }
|
||||
.input-row { display: flex; gap: 8px; margin-bottom: 12px; }
|
||||
input[type="url"] {
|
||||
flex: 1; padding: 10px 14px; border: 1px solid #ddd;
|
||||
border-radius: 8px; font-size: 14px; outline: none;
|
||||
}
|
||||
input[type="url"]:focus { border-color: #999; }
|
||||
button {
|
||||
padding: 10px 20px; background: #1a1a1a; color: #fff;
|
||||
border: none; border-radius: 8px; font-size: 14px; cursor: pointer;
|
||||
}
|
||||
button:hover { background: #333; }
|
||||
button:disabled { background: #999; cursor: not-allowed; }
|
||||
.error { color: #c00; font-size: 13px; margin-bottom: 24px; min-height: 18px; }
|
||||
.articles { display: flex; flex-direction: column; gap: 24px; margin-top: 24px; }
|
||||
.article { padding-bottom: 24px; border-bottom: 1px solid #eee; }
|
||||
.article:last-child { border-bottom: none; }
|
||||
.article-url { font-size: 12px; color: #999; word-break: break-all; margin-bottom: 4px; }
|
||||
.article-title { font-size: 16px; font-weight: 600; margin-bottom: 8px; }
|
||||
.article-summary { font-size: 14px; color: #555; line-height: 1.6; }
|
||||
.article-date { font-size: 12px; color: #bbb; margin-top: 8px; }
|
||||
.empty { color: #bbb; font-size: 14px; text-align: center; padding: 48px 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Read it later</h1>
|
||||
<form class="input-row" id="form">
|
||||
<input type="url" id="url" placeholder="Paste article URL..." required>
|
||||
<button type="submit" id="btn">Save</button>
|
||||
</form>
|
||||
<div id="error" class="error"></div>
|
||||
<div class="articles" id="articles"></div>
|
||||
<script>
|
||||
const API = '/api';
|
||||
|
||||
async function load() {
|
||||
try {
|
||||
const r = await fetch(API + '/articles');
|
||||
const data = await r.json();
|
||||
const el = document.getElementById('articles');
|
||||
if (!data.length) { el.innerHTML = '<div class="empty">No articles yet</div>'; return; }
|
||||
el.innerHTML = data.map(a => `
|
||||
<div class="article">
|
||||
<div class="article-url">${a.url}</div>
|
||||
<div class="article-title">${a.title || 'Untitled'}</div>
|
||||
<div class="article-summary">${a.summary || 'Processing...'}</div>
|
||||
<div class="article-date">${new Date(a.created_at).toLocaleDateString()}</div>
|
||||
</div>`).join('');
|
||||
} catch (e) {
|
||||
document.getElementById('articles').innerHTML = '<div class="empty">Failed to load</div>';
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const btn = document.getElementById('btn');
|
||||
const inp = document.getElementById('url');
|
||||
const err = document.getElementById('error');
|
||||
btn.disabled = true; btn.textContent = '...'; err.textContent = '';
|
||||
try {
|
||||
const r = await fetch(API + '/articles', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({url: inp.value})
|
||||
});
|
||||
if (!r.ok) throw new Error((await r.json()).detail || 'Error');
|
||||
inp.value = '';
|
||||
await load();
|
||||
} catch (e) { err.textContent = e.message; }
|
||||
finally { btn.disabled = false; btn.textContent = 'Save'; }
|
||||
});
|
||||
|
||||
load();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
45
sk1/web/nginx/nginx.conf
Normal file
45
sk1/web/nginx/nginx.conf
Normal file
@ -0,0 +1,45 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name devopspavel.me;
|
||||
|
||||
location /.well-known/acme-challenge/ {
|
||||
root /var/www/certbot;
|
||||
}
|
||||
|
||||
location / {
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name devopspavel.me;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/devopspavel.me/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/devopspavel.me/privkey.pem;
|
||||
|
||||
# SSL Best Practices
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_prefer_server_ciphers on;
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://api:8000;
|
||||
proxy_http_version 1.1;
|
||||
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;
|
||||
proxy_read_timeout 120s;
|
||||
}
|
||||
|
||||
location /health {
|
||||
return 200 'OK';
|
||||
add_header Content-Type text/plain;
|
||||
}
|
||||
}
|
||||
@ -1,3 +1,2 @@
|
||||
FROM nginx:latest
|
||||
COPY ./nginx/nginx.conf /etc/nginx/nginx.conf
|
||||
COPY ./frontend/index.html /usr/share/nginx/html/index.html
|
||||
|
||||
@ -1,66 +0,0 @@
|
||||
worker_processes auto;
|
||||
|
||||
worker_rlimit_nofile 1035;
|
||||
pid /var/run/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
multi_accept on;
|
||||
}
|
||||
|
||||
|
||||
http {
|
||||
|
||||
upstream backend {
|
||||
server backend:5000;
|
||||
}
|
||||
|
||||
charset utf-8;
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout 65;
|
||||
types_hash_max_size 2048;
|
||||
|
||||
open_file_cache max=1000 inactive=20s;
|
||||
open_file_cache_valid 30s;
|
||||
open_file_cache_min_uses 2;
|
||||
open_file_cache_errors on;
|
||||
|
||||
client_max_body_size 10m;
|
||||
client_body_buffer_size 16k;
|
||||
client_header_buffer_size 1k;
|
||||
large_client_header_buffers 2 1k;
|
||||
|
||||
client_body_timeout 12;
|
||||
client_header_timeout 12;
|
||||
|
||||
send_timeout 10;
|
||||
|
||||
server_tokens off;
|
||||
|
||||
include /etc/nginx/mime.types;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
error_log /var/log/nginx/static_errors.log debug;
|
||||
access_log off;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
location /api/ {
|
||||
proxy_pass http://backend/;
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user