Compare commits

...

7 Commits

Author SHA1 Message Date
16f3e17a3d Анекдот: Две черепахи, Dev и Ops, пытаются пересечь дорогу. Dev добегает до середины, выкладывает новый релиз и убегает обратно. Ops все еще у стартовой линии: Ну да, а как это теперь все поддерживать?! Анекдот писала бездушная машина 2025-04-10 17:02:08 -04:00
47d445fb8a щими щими ей щим е щими я 2025-03-31 21:44:35 +03:00
44e2a33661 эээ вы не могли бы подписать мою петицию 2025-03-30 22:42:43 +03:00
ccd7794b53 Эщкере чюпеп крокодило бомбардило это самый информативный коммит 2025-03-30 21:58:13 +03:00
ffc73c7657 дубль два тащим все в мастер, решать надо или нет потом будем
а если серьезно, то внесенные изменения:
почти полность изменил структуру проекта, чувствительные данные теперь находятся в файле .env,
немного поправил docker-compose.yml в сторону best practice
2025-03-29 23:37:12 +03:00
51e0019fd6 все в мастер тащим, решать надо или нет потом будем 2025-03-29 23:29:34 +03:00
00bdcf781f Merge branch 'Develop' into 'main'
Develop

See merge request 3-sa/WebServer!1
2025-03-20 21:28:45 +00:00
19 changed files with 350 additions and 231 deletions

10
.env Normal file
View File

@ -0,0 +1,10 @@
#Пример .env файла. Используем его чтобы не указывать чувствительные данные (пароли, ключи, API и т.д.) напрямую в коде
# PostgreSQL
POSTGRES_USER=user
POSTGRES_PASSWORD=password
POSTGRES_DB=website
DB_HOST=database
# Nginx bublick & private key
SSL_CERT_FILE=./nginx/ssl/domain.crt
SSL_KEY_FILE=./nginx/ssl/domain.key

View File

@ -0,0 +1,29 @@
name: Deploy to Server
on:
branches:
- master
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup SSH Key
uses: webfactory/ssh-agent@v0.7.0
with:
ssh-private-key: ${{ secrets.DEPLOY_SSH_KEY }}
- name: Add Known Host
run: ssh-keyscan ${{ secrets.SERVER_IP }} >> ~/.ssh/known_hosts
- name: Deploy to Server
run: |
ssh -o StrictHostKeyChecking=no ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_IP }} "
cd ${{ secrets.PROJECT_PATH }} &&
git pull origin master &&
docker-compose down --remove-orphans &&
docker-compose up -d --build
"

View File

@ -1 +1,72 @@
Первый пуш, не гарантирую работоспособность этого кода
# 🚀 Web Application with Docker, PostgreSQL & Flask
Простое веб-приложение с авторизацией, которое можно использовать как основу для собственного проекта.
## 🌟 Особенности проекта
- **Docker-ориентированный** (сборка через compose)
- **Безопасность**: секреты через ".env", healthcheck для БД
- **Автоматический деплой** через Git hooks
- **Логирование** операций деплоя
## 🛠 Технологический стек
- Frontend: Nginx со статическим html
- Backend: Python Flask
- DataBase: PostgreSQL
- Infrastructure: Docker, Docker Compose
- CI/CD: Git hooks
🔧 Архитектура проекта
Copy
container-web-app/
├── backend/
│ ├── requirements.txt
│ └── server.py
├── db/
│ └── init.sql
├── frontend/
│ ├── static/
│ └── index.html
├── nginx/
│ ├── ssl/
│ │ ├── domain.crt
│ │ ├── domain.conf
│ │ └── domain.key
│ └── nginx.conf
├── scripts/
│ └── post-receive
├── .env
├── docker-compose.yaml
└── README.md
🔒 Безопасность
Все секреты хранятся в .env (в git не коммитятся)
Healthcheck для мониторинга состояния PostgreSQL
## CI/CD:
Автоматический деплой через Git hooks
Скрипт деплоя с логированием (/var/log/deploy.log)
## Infrastructure as Code:
Полная воспроизводимость через Docker
Версионирование образов
## 📈 Дальнейшее развитие
- Добавить тесты (pytest)
- Настроить GitHub Actions для CI
- Реализовать балансировку нагрузки
- Добавить мониторинг (Prometheus + Grafana)

View File

@ -1,2 +1,3 @@
Flask
psycopg2-binary
psycopg2-binary
python-dotenv

View File

@ -1,20 +1,26 @@
from flask import Flask, request, render_template
import psycopg2
import os
from dotenv import load_dotenv
load_dotenv()
app = Flask(__name__, template_folder='/media/frontend')
DATABASE = {
'dbname': 'WebSite',
'user': 'User',
'password': 'Password',
'host': 'DataBase',
'dbname': os.getenv('POSTGRES_DB'),
'user': os.getenv('POSTGRES_USER'),
'password': os.getenv('POSTGRES_PASSWORD'),
'host': os.getenv('DB_HOST')
}
def get_db_connection():
conn = psycopg2.connect(**DATABASE)
return conn
try:
conn = psycopg2.connect(**DATABASE)
return conn
except psycopg2.Error as e:
print(f"Ошибка подключения к базе данных: {e}")
raise
@app.route('/', methods=['GET'])
def index():
@ -23,45 +29,50 @@ def index():
@app.route('/submit', methods=['POST'])
def submit():
action = request.form.get('action')
message = ""
if action == 'Login':
username = request.form.get('username')
password = request.form.get('password')
try:
conn = get_db_connection()
cur = conn.cursor()
cur.execute('SELECT * FROM users WHERE username = %s AND password = %s', (username, password))
user = cur.fetchone()
if action == 'Login':
username = request.form.get('username')
password = request.form.get('password')
cur.close()
conn.close()
cur.execute(
'SELECT * FROM users WHERE username = %s AND password = %s',
(username, password)
)
user = cur.fetchone()
message = "Успешный вход!" if user else "Неправильные имя пользователя или пароль!"
elif action == 'Register':
new_username = request.form.get('new_username')
new_password = request.form.get('new_password')
cur.execute('SELECT * FROM users WHERE username = %s', (new_username,))
if cur.fetchone():
message = "Пользователь уже существует!"
else:
cur.execute(
'INSERT INTO users (username, password) VALUES (%s, %s)',
(new_username, new_password)
)
conn.commit()
message = "Успешная регистрация!"
if user:
message = "Login successful!"
else:
message = "Invalid username or password."
message = "Неизвестное действие"
elif action == 'Register':
new_username = request.form.get('new_username')
new_password = request.form.get('new_password')
conn = get_db_connection()
cur = conn.cursor()
cur.execute('SELECT * FROM users WHERE username = %s', (new_username,))
if cur.fetchone():
message = "User already exists!"
else:
cur.execute('INSERT INTO users (username, password) VALUES (%s, %s)', (new_username, new_password))
conn.commit()
message = "Registration successful!"
cur.close()
conn.close()
else:
message = "Invalid action."
except psycopg2.Error as e:
conn.rollback()
message = f"Ошибка базы данных: {e}"
finally:
if 'cur' in locals():
cur.close()
if 'conn' in locals():
conn.close()
return render_template('index.html', message=message)

View File

@ -1 +0,0 @@
Наш сертификат

View File

@ -1 +0,0 @@
Приватный ключ

View File

@ -1,5 +1,5 @@
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
password VARCHAR(50) NOT NULL
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
password VARCHAR(50) NOT NULL
);

51
docker-compose.yaml Normal file
View File

@ -0,0 +1,51 @@
services:
database:
image: postgres:17.4-alpine3.21
container_name: database
ports:
- "5432:5432"
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
#Монтируем директорию на хосте, чтобы при повторных "docker-compose up" таблицы с нашими данными сохранялись
volumes:
- /database:/var/lib/postgresql/data
cpus: '0.15'
mem_limit: 256M
healthcheck:
test: ["CMD-SHELL", "pg_isready -U admin -WebSite"]
interval: 5s
timeout: 5s
retries: 5
webserver:
image: nginx:1.27.4-alpine
container_name: webserver
ports:
- "80:80"
- "443:443"
volumes:
- /WebApp/frontend:/usr/share/nginx/html
- /WebApp/nginx/ssl:/etc/nginx/sites-available
- /WebApp/nginx/nginx.conf:/etc/nginx/nginx.conf
depends_on:
- database
- backend
cpus: '0.15'
mem_limit: 256M
backend:
image: python:3.9
container_name: backend_part
ports:
- "5000:5000"
volumes:
- /WebApp/backend:/media/backend
working_dir: /media/backend
command: >
sh -c "pip install -r requirements.txt && python server.py"
cpus: '0.35'
mem_limit: 256M

View File

@ -1,51 +0,0 @@
services:
DataBase:
image: postgres:latest
container_name: DataBase
ports:
- "5432:5432"
#Так никто не делает в реальных кейсах, я просто даун, не умею работать с секретами (опция secrets)
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: password
POSTGRES_DB: WebSite
volumes:
- /home/git/myprojects/database:/var/lib/postgresql/data
cpus: '0.15'
mem_limit: 256M
healthcheck:
test: ["CMD-SHELL", "pg_isready -U admin -WebSite"]
interval: 5s
timeout: 5s
retries: 5
webserver:
image: nginx:latest
container_name: WebServer
ports:
- "80:80"
- "443:443"
volumes:
- /home/git/myprojects/WorkServer/frontend:/usr/share/nginx/html
- /home/git/myprojects/WorkServer/conf:/etc/nginx/sites-available
depends_on:
- DataBase
- backend
cpus: '0.15'
mem_limit: 256M
backend:
image: python:3.9
container_name: backend_part
ports:
- "5000:5000"
volumes:
- /home/git/myprojects/WorkServer/backend:/media/backend
- /home/git/myprojects/WorkServer/frontend:/media/frontend
working_dir: /media/backend
command: >
sh -c "pip install -r requirements.txt && python server.py"
cpus: '0.35'
mem_limit: 256M

View File

@ -1,3 +0,0 @@
FROM nginx:v1
COPY nginx.conf /etc/nginx/nginx.conf

View File

@ -1,36 +1,36 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login and Register Form</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="form-container">
<h1>Login Form</h1>
<form action="/submit" method="post">
<label for="username">Username:</label><br>
<input type="text" id="username" name="username" required><br><br>
<label for="password">Password:</label><br>
<input type="password" id="password" name="password" required><br><br>
<button type="submit" name="action" value="Login">Login</button>
</form>
</div>
<div class="form-container">
<h1>Register Form</h1>
<form action="/submit" method="post">
<label for="new_username">Username:</label><br>
<input type="text" id="new_username" name="new_username" required><br><br>
<label for="new_password">Password:</label><br>
<input type="password" id="new_password" name="new_password" required><br><br>
<button type="submit" name="action" value="Register">Register</button>
</form>
</div>
</body>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login and Register Form</title>
<link rel="stylesheet" href="./static/css/styles.css">
</head>
<body>
<div class="form-container">
<h1>Login Form</h1>
<form action="/submit" method="post">
<label for="username">Username:</label><br>
<input type="text" id="username" name="username" required><br><br>
<label for="password">Password:</label><br>
<input type="password" id="password" name="password" required><br><br>
<button type="submit" name="action" value="Login">Login</button>
</form>
</div>
<div class="form-container">
<h1>Register Form</h1>
<form action="/submit" method="post">
<label for="new_username">Username:</label><br>
<input type="text" id="new_username" name="new_username" required><br><br>
<label for="new_password">Password:</label><br>
<input type="password" id="new_password" name="new_password" required><br><br>
<button type="submit" name="action" value="Register">Register</button>
</form>
</div>
</body>
</html>

View File

@ -1,32 +1,32 @@
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
#gzip on;
include /etc/nginx/sites-available/domain.conf;
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
#gzip on;
include /etc/nginx/sites-available/domain.conf;
}

View File

@ -1,44 +1,40 @@
server {
listen 80;
server_name your_domain.com;
return 301 https://your_domain.com$request_uri;
}
server {
listen 443 ssl;
server_name your_domain.com;
ssl_certificate /etc/nginx/sites-available/domain.crt;
ssl_certificate_key /etc/nginx/sites-available/domain.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
root /usr/share/nginx/html;
location / {
try_files $uri $uri/ /index.html;
}
location /submit {
proxy_pass http://backend_part:5000;
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 {
proxy_pass http://backend_part:5000/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
server {
listen 80;
server_name your_doman.com www.your_doman.com;
return 301 https://your_doman.com$request_uri;
}
server {
listen 443 ssl;
server_name your_doman.com www.your_doman.com;
ssl_certificate /etc/nginx/sites-available/domain.crt;
ssl_certificate_key /etc/nginx/sites-available/domain.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
root /usr/share/nginx/html;
location / {
try_files $uri $uri/ /index.html;
}
location /submit {
proxy_pass http://backend_part:5000;
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;
}
}

1
nginx/ssl/domain.crt Normal file
View File

@ -0,0 +1 @@
Наш публичный ключ

1
nginx/ssl/domain.key Normal file
View File

@ -0,0 +1 @@
Наш приватный ключ

View File

@ -1,21 +0,0 @@
#!/bin/bash
TARGET="/home/git/myprojects/WorkServer"
GIT_DIR="/home/git/myprojects/web"
BRANCH="master"
git --work-tree="$TARGET" --git-dir="$GIT_DIR" checkout -f "$BRANCH"
cd /home/git/myprojects/WorkServer/docker
#docker-compose block:
# 1
docker-compose build
# 2
docker-compose down
# 3
docker-compose up -d

25
scripts/post-receive Normal file
View File

@ -0,0 +1,25 @@
#!/bin/bash
TARGET="path to work repo"
GIT_DIR="Path to .git repo"
BRANCH="master"
LOG_FILE="/var/log/deploy.log"
exec > >(tee -a "$LOG_FILE") 2>&1
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Starting deploy..."
git --work-tree="$TARGET" --git-dir="$GIT_DIR" checkout -f "$BRANCH"
cd $TARGET
#docker-compose block:
# 1
docker-compose build
# 2
docker-compose down
# 3
docker-compose up -d
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Deploy successful!"