From 715bc90340a2e526b787bf308ced1764f979003c Mon Sep 17 00:00:00 2001 From: Giovani Date: Wed, 23 Jul 2025 22:00:35 +0000 Subject: [PATCH] feat: setup project for docker deploy --- .gitignore | 1 + .env.example => backend/.env.example | 0 backend/Dockerfile | 10 ++ backend/config.py | 6 +- backend/main.py | 5 +- requirements.txt => backend/requirements.txt | 0 backend/routes/shortener.py | 2 +- frontend/.env.example | 1 - frontend/Dockerfile | 8 ++ frontend/src/api/url/urlApi.ts | 2 +- frontend/src/features/url/urlSlice.ts | 2 +- generate_configs.py | 112 +++++++++++++++++++ 12 files changed, 139 insertions(+), 10 deletions(-) rename .env.example => backend/.env.example (100%) create mode 100644 backend/Dockerfile rename requirements.txt => backend/requirements.txt (100%) create mode 100644 frontend/Dockerfile create mode 100644 generate_configs.py diff --git a/.gitignore b/.gitignore index a91b614..4df28e6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # Environments .env +.env.*.local .venv env/ venv/ diff --git a/.env.example b/backend/.env.example similarity index 100% rename from .env.example rename to backend/.env.example diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..628cc31 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,10 @@ +FROM python:3.11-slim + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/backend/config.py b/backend/config.py index b0fabe0..6a814af 100644 --- a/backend/config.py +++ b/backend/config.py @@ -1,5 +1,6 @@ from pydantic_settings import BaseSettings from typing import Literal, List +import os class Settings(BaseSettings): database_url: str @@ -9,12 +10,11 @@ class Settings(BaseSettings): allow_origins: str class Config: - env_file = ".env" + env_file = os.path.join(os.path.dirname(__file__), ".env") env_file_encoding = "utf-8" def get_allow_origins(self) -> List[str]: return [origin.strip() for origin in self.allow_origins.split(",")] - # Singleton instance of settings -settings = Settings() \ No newline at end of file +settings = Settings() diff --git a/backend/main.py b/backend/main.py index 7f0dcd2..da43c69 100644 --- a/backend/main.py +++ b/backend/main.py @@ -19,9 +19,8 @@ app.add_middleware( async def startup(): await database.connect() - # only create missing tables in dev environment - if settings.environment == "dev": - Base.metadata.create_all(bind=engine) + # create tables if they don't exist + Base.metadata.create_all(bind=engine) @app.on_event("shutdown") async def shutdown(): diff --git a/requirements.txt b/backend/requirements.txt similarity index 100% rename from requirements.txt rename to backend/requirements.txt diff --git a/backend/routes/shortener.py b/backend/routes/shortener.py index 5b8afa6..950a416 100644 --- a/backend/routes/shortener.py +++ b/backend/routes/shortener.py @@ -22,7 +22,7 @@ async def redirect_to_original_url(shortcode: str): raise HTTPException(status_code=404, detail=str(e)) from e -@router.post("/") +@router.post("/api/shorten") async def create_url_shortcode(url_payload: UrlPayload): url = str(url_payload.url) diff --git a/frontend/.env.example b/frontend/.env.example index 4fdc244..88a2725 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -1,4 +1,3 @@ # Frontend environment variables # REACT_APP_API_BASE_URL=http://localhost:8000 -REACT_APP_BASE_URL=http://localhost:8000 diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..d54402b --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,8 @@ +FROM node:20-alpine AS builder + +WORKDIR /app +COPY . . +RUN npm install && npm run build + +FROM nginx:alpine +COPY --from=builder /app/build /usr/share/nginx/html diff --git a/frontend/src/api/url/urlApi.ts b/frontend/src/api/url/urlApi.ts index 1bfce52..cdc86e3 100644 --- a/frontend/src/api/url/urlApi.ts +++ b/frontend/src/api/url/urlApi.ts @@ -5,7 +5,7 @@ const baseUrl = process.env.REACT_APP_API_BASE_URL export async function shortenUrlApi( payload: ShortenUrlRequest ): Promise { - const response = await fetch(`${baseUrl}/`, { + const response = await fetch(`${baseUrl}/api/shorten`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), diff --git a/frontend/src/features/url/urlSlice.ts b/frontend/src/features/url/urlSlice.ts index 9393851..120e241 100644 --- a/frontend/src/features/url/urlSlice.ts +++ b/frontend/src/features/url/urlSlice.ts @@ -6,7 +6,7 @@ import { UrlState } from './types'; export const shortenUrl = createAsyncThunk( 'url/shortenUrl', async (url: string) => { - const baseUrl = process.env.REACT_APP_BASE_URL + const baseUrl = process.env.REACT_APP_API_BASE_URL const data = await shortenUrlApi({ url }); return `${baseUrl}/${data.shortcode}`; } diff --git a/generate_configs.py b/generate_configs.py new file mode 100644 index 0000000..d55f0fc --- /dev/null +++ b/generate_configs.py @@ -0,0 +1,112 @@ +import secrets +import random +from pathlib import Path + +# === Prompt user input === # +react_api_url = input("šŸ”§ Enter the REACT_APP_API_BASE_URL (e.g. https://minxa.wtf): ").strip() +allowed_origins = input("šŸ”’ Enter allowed CORS origins for the backend comma-separated (e.g. https://minxa.lol,https://minxo.lol): ").strip() + +# Ask for environment and validate input +valid_envs = ["dev", "stage", "prod"] +while True: + environment = input("šŸŒŽ Enter environment (dev, stage, prod): ").strip().lower() + if environment in valid_envs: + break + print("āŒ Invalid input. Please enter one of: dev, stage, prod") + +# === Backend values === # +db_user = "minxa" +db_password = secrets.token_urlsafe(16) +db_name = "minxadb" +db_host = "minxa-db" +db_port = 5432 + +hashids_salt = secrets.token_urlsafe(32) +alphabet = list("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456789") +random.shuffle(alphabet) +encoder_alphabet = "".join(alphabet) + +# === Generate backend .env === # +backend_env = f"""# Auto-generated backend .env + +DATABASE_URL=postgresql+asyncpg://{db_user}:{db_password}@{db_host}:{db_port}/{db_name} +ENVIRONMENT={environment} +HASHIDS_SALT={hashids_salt} +ENCODER_ALPHABET={encoder_alphabet} +ALLOWED_ORIGINS={allowed_origins} +""" + +backend_env_path = Path("backend/.env") +backend_env_path.parent.mkdir(parents=True, exist_ok=True) +backend_env_path.write_text(backend_env) +print(f"āœ… backend/.env written") + +# === Generate frontend .env === # +frontend_env = f"""# Auto-generated frontend .env + +REACT_APP_API_BASE_URL={react_api_url} +""" + +frontend_env_path = Path("frontend/.env") +frontend_env_path.parent.mkdir(parents=True, exist_ok=True) +frontend_env_path.write_text(frontend_env) +print(f"āœ… frontend/.env written") + +# === Generate docker-compose.generated.yml === # +compose_yml = f"""version: "3.9" + +services: + minxa-db: + image: postgres:16 + environment: + POSTGRES_USER: {db_user} + POSTGRES_PASSWORD: {db_password} + POSTGRES_DB: {db_name} + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD", "pg_isready", "-U", "{db_user}"] + interval: 5s + timeout: 5s + retries: 5 + networks: + - appnet + + minxa-backend: + build: + context: ./backend + env_file: + - ./backend/.env + depends_on: + minxa-db: + condition: service_healthy + ports: + - "8000:8000" + networks: + - appnet + + minxa-frontend: + build: + context: ./frontend + env_file: + - ./frontend/.env + depends_on: + - minxa-backend + ports: + - "3000:80" + networks: + - appnet + +volumes: + postgres_data: + +networks: + appnet: +""" + +compose_path = Path("docker-compose.generated.yml") +compose_path.write_text(compose_yml) +print(f"āœ… docker-compose.generated.yml written") + +print("\nšŸŽ‰ All files generated! Run your stack with:\n") +print("docker compose -f docker-compose.generated.yml up --build")