This commit is contained in:
Павел Лопатин 2024-08-12 22:22:51 +03:00
commit e817baab55
28 changed files with 509 additions and 0 deletions

14
.env.example Normal file
View File

@ -0,0 +1,14 @@
SECRET_KEY=secret
DEBUG=1
CSRF_TRUSTED_ORIGINS=http://localhost:8000 http://127.0.0.1:8000
ALLOWED_HOSTS=*
DB_NAME=postgres
DB_USER=postgres
DB_PASSWORD=postgres
DB_HOST=db
DB_PORT=5432
REDIS_HOST=redis
REDIS_PORT=6379
REDIS_PASSWORD=redis

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
static
.env
venv
.idea

19
Dockerfile Normal file
View File

@ -0,0 +1,19 @@
FROM python:3.11.6-slim
# set work directory
WORKDIR /app
# set env variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
RUN apt-get update
RUN apt-get install -y python3-dev gcc libc-dev libffi-dev
RUN apt-get -y install libpq-dev gcc
# install dependencies
COPY src/requirements.txt .
RUN pip install --upgrade pip
RUN pip install -r requirements.txt
# copy project
COPY src/. .

13
README.md Normal file
View File

@ -0,0 +1,13 @@
# simpliest django(uvicorn)+postgresql+fastapi+redis+nginx docker-compose (ready for production and dev)
To run:
`docker-compose up -d`
Site available on 8000 port.
You can make any changes in code, they will appear automatically. If you want to execute something with manage.py use:
```
docker-compose exec app python3 manage.py migrate
docker-compose exec app python3 manage.py makemigrations
docker-compose exec app python3 manage.py update_admin admin adminpass # create superuser
```
and so on.

66
docker-compose.yaml Normal file
View File

@ -0,0 +1,66 @@
services:
db:
image: postgres:latest
command: postgres -c 'max_connections=200'
environment:
- POSTGRES_USER=${DB_USER}
- POSTGRES_PASSWORD=${DB_PASSWORD}
- POSTGRES_DB=${DB_NAME}
volumes:
- postgresql-data:/var/lib/postgresql/data
restart: on-failure
nginx:
command: nginx -g "daemon off;"
depends_on:
- app
- api
image: nginx:alpine
restart: on-failure
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
- static:/var/www/app/static
ports:
- "8000:8000"
app:
build:
context: .
dockerfile: Dockerfile
command: bash -c 'while !</dev/tcp/db/5432; do sleep 1; done; python3 manage.py collectstatic --no-input; python3 manage.py migrate; uvicorn core.asgi:application --port 8000 --host 0.0.0.0'
volumes:
- ./src/:/app/
- static:/app/static
depends_on:
- db
- redis
restart: on-failure
env_file:
- .env
api:
build:
context: .
dockerfile: Dockerfile
command: bash -c 'uvicorn core.asgi:fastapp --port 8000 --host 0.0.0.0'
volumes:
- ./src/:/app/
depends_on:
- db
- redis
restart: on-failure
env_file:
- .env
redis:
image: redis:latest
command: redis-server --requirepass ${REDIS_PASSWORD}
volumes:
- redis-data:/data
restart: on-failure
volumes:
postgresql-data:
static:
redis-data:
external: false

60
nginx/nginx.conf Normal file
View File

@ -0,0 +1,60 @@
# nginx.conf
user nginx;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 5s;
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;
upstream app {
server app:8000;
}
upstream api {
server api:8000;
}
server {
listen 8000;
charset utf-8;
server_name _;
location /static/ {
autoindex on;
alias /var/www/app/static/;
}
location /django/ {
proxy_redirect off;
proxy_set_header Host app;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $server_name;
proxy_pass http://app;
}
location / {
proxy_redirect off;
proxy_set_header Host app;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $server_name;
proxy_pass http://api;
}
}
}

View File

3
src/application/admin.py Normal file
View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
src/application/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class MainConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'application'

View File

View File

@ -0,0 +1,32 @@
from django.core.management.base import BaseCommand
from django.contrib.auth import get_user_model
class Command(BaseCommand):
def add_arguments(self, parser):
# Positional arguments
parser.add_argument('username', type=str)
parser.add_argument('password', type=str)
parser.add_argument('--email', type=str, default='')
def handle(self, *args, **options):
user_model = get_user_model()
message = None
try:
user = user_model.objects.get(username=options["username"])
if not user.check_password(options["password"]):
user.set_password(options["password"])
user.save()
message = "[INFO] Admin password has been updated"
except user_model.DoesNotExist:
user = user_model(
username=options["username"],
email=options["email"],
is_superuser=True,
is_staff=True
)
user.set_password(options["password"])
user.save()
message = "[INFO] Admin has been created"
return message

View File

View File

@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

View File

@ -0,0 +1,3 @@
from fastapi import APIRouter
router = APIRouter()

View File

@ -0,0 +1,14 @@
from starlette import status
from starlette.responses import JSONResponse
from . import router
@router.get("/example")
async def example():
return JSONResponse(
status_code=status.HTTP_200_OK,
content={
"status": "OK"
}
)

8
src/cache/__init__.py vendored Normal file
View File

@ -0,0 +1,8 @@
from django.conf import settings
from cache.provider import RedisOverride
redis = RedisOverride(
host=settings.REDIS_HOST, port=settings.REDIS_PORT,
password=settings.REDIS_PASSWORD
)

32
src/cache/provider.py vendored Normal file
View File

@ -0,0 +1,32 @@
import contextlib
import json
from typing import Union
from redis import Redis
class RedisOverride:
"""
redis init
"""
def __init__(self, host: str, port: int, password: str):
self.redis = Redis(host=host, port=port, password=password)
def set(self, key: str, value: Union[str, dict, list, int]):
if type(value) is not str:
value = json.dumps(value)
self.redis.set(key, value)
def get(self, key: str, default=None) -> Union[str, dict, int, list, None]:
value = self.redis.get(key)
if value is None:
return default
value = value.decode("utf-8")
with contextlib.suppress(Exception):
value = json.loads(value)
return value
def delete(self, key: str):
self.redis.delete(key)

0
src/core/__init__.py Normal file
View File

31
src/core/asgi.py Normal file
View File

@ -0,0 +1,31 @@
"""
ASGI config for core project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "core.settings")
application = get_asgi_application()
from application.routers.api import router
fastapp = FastAPI()
fastapp.include_router(router)
fastapp.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)

100
src/core/settings.py Normal file
View File

@ -0,0 +1,100 @@
import os
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
SECRET_KEY = os.getenv("SECRET_KEY")
DEBUG = bool(int(os.getenv("DEBUG", 1)))
ALLOWED_HOSTS = os.getenv("ALLOWED_HOSTS", "*").split(" ")
CSRF_TRUSTED_ORIGINS = os.getenv("CSRF_TRUSTED_ORIGINS", "http://* https://*").split(" ")
REDIS_HOST = os.getenv("REDIS_HOST")
REDIS_PORT = int(os.getenv("REDIS_PORT"))
REDIS_PASSWORD = os.getenv("REDIS_PASSWORD")
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'application'
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'core.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'core.wsgi.application'
ASGI_APPLICATION = "core.asgi.application"
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql_psycopg2",
"NAME": os.getenv("DB_NAME"),
"USER": os.getenv("DB_USER"),
"PASSWORD": os.getenv("DB_PASSWORD"),
"HOST": os.getenv("DB_HOST"),
"PORT": os.getenv("DB_PORT"),
}
}
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true"

21
src/core/urls.py Normal file
View File

@ -0,0 +1,21 @@
"""core URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/3.2/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path
urlpatterns = [
path('django/admin/', admin.site.urls),
]

16
src/core/wsgi.py Normal file
View File

@ -0,0 +1,16 @@
"""
WSGI config for core project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')
application = get_wsgi_application()

0
src/helpers/__init__.py Normal file
View File

View File

View File

@ -0,0 +1,37 @@
import json
from django.db import models
class JSONField(models.TextField):
"""JSONField is a generic textfield that neatly serializes/unserializes
JSON objects seamlessly"""
def to_python(self, value):
"""Convert our string value to JSON after we load it from the DB"""
if value == "" or value is None:
return None
elif isinstance(value, str):
value = json.loads(value)
else:
raise TypeError("Not valid value type in db")
return value
def from_db_value(self, value, expression, connection):
return self.to_python(value)
def get_prep_value(self, value):
"""Convert our JSON object to a string before we save"""
if isinstance(value, (dict, list)):
value = json.dumps(value)
elif value == "" or value is None:
pass
else:
raise TypeError(f"Not valid value type. ValueType={type(value)}")
return value
def value_from_object(self, obj):
return json.dumps(super().value_from_object(obj))

22
src/manage.py Normal file
View File

@ -0,0 +1,22 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()

5
src/requirements.txt Normal file
View File

@ -0,0 +1,5 @@
uvicorn==0.27.1
fastapi==0.109.0
Django==5.0.2
psycopg2
redis==4.6.0