feat: setup project for docker deploy
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,5 +1,6 @@
|
|||||||
# Environments
|
# Environments
|
||||||
.env
|
.env
|
||||||
|
.env.*.local
|
||||||
.venv
|
.venv
|
||||||
env/
|
env/
|
||||||
venv/
|
venv/
|
||||||
|
|||||||
10
backend/Dockerfile
Normal file
10
backend/Dockerfile
Normal file
@@ -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"]
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
from pydantic_settings import BaseSettings
|
from pydantic_settings import BaseSettings
|
||||||
from typing import Literal, List
|
from typing import Literal, List
|
||||||
|
import os
|
||||||
|
|
||||||
class Settings(BaseSettings):
|
class Settings(BaseSettings):
|
||||||
database_url: str
|
database_url: str
|
||||||
@@ -9,12 +10,11 @@ class Settings(BaseSettings):
|
|||||||
allow_origins: str
|
allow_origins: str
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
env_file = ".env"
|
env_file = os.path.join(os.path.dirname(__file__), ".env")
|
||||||
env_file_encoding = "utf-8"
|
env_file_encoding = "utf-8"
|
||||||
|
|
||||||
def get_allow_origins(self) -> List[str]:
|
def get_allow_origins(self) -> List[str]:
|
||||||
return [origin.strip() for origin in self.allow_origins.split(",")]
|
return [origin.strip() for origin in self.allow_origins.split(",")]
|
||||||
|
|
||||||
|
|
||||||
# Singleton instance of settings
|
# Singleton instance of settings
|
||||||
settings = Settings()
|
settings = Settings()
|
||||||
|
|||||||
@@ -19,9 +19,8 @@ app.add_middleware(
|
|||||||
async def startup():
|
async def startup():
|
||||||
await database.connect()
|
await database.connect()
|
||||||
|
|
||||||
# only create missing tables in dev environment
|
# create tables if they don't exist
|
||||||
if settings.environment == "dev":
|
Base.metadata.create_all(bind=engine)
|
||||||
Base.metadata.create_all(bind=engine)
|
|
||||||
|
|
||||||
@app.on_event("shutdown")
|
@app.on_event("shutdown")
|
||||||
async def shutdown():
|
async def shutdown():
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ async def redirect_to_original_url(shortcode: str):
|
|||||||
raise HTTPException(status_code=404, detail=str(e)) from e
|
raise HTTPException(status_code=404, detail=str(e)) from e
|
||||||
|
|
||||||
|
|
||||||
@router.post("/")
|
@router.post("/api/shorten")
|
||||||
async def create_url_shortcode(url_payload: UrlPayload):
|
async def create_url_shortcode(url_payload: UrlPayload):
|
||||||
url = str(url_payload.url)
|
url = str(url_payload.url)
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
# Frontend environment variables #
|
# Frontend environment variables #
|
||||||
|
|
||||||
REACT_APP_API_BASE_URL=http://localhost:8000
|
REACT_APP_API_BASE_URL=http://localhost:8000
|
||||||
REACT_APP_BASE_URL=http://localhost:8000
|
|
||||||
|
|||||||
8
frontend/Dockerfile
Normal file
8
frontend/Dockerfile
Normal file
@@ -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
|
||||||
@@ -5,7 +5,7 @@ const baseUrl = process.env.REACT_APP_API_BASE_URL
|
|||||||
export async function shortenUrlApi(
|
export async function shortenUrlApi(
|
||||||
payload: ShortenUrlRequest
|
payload: ShortenUrlRequest
|
||||||
): Promise<ShortenUrlResponse> {
|
): Promise<ShortenUrlResponse> {
|
||||||
const response = await fetch(`${baseUrl}/`, {
|
const response = await fetch(`${baseUrl}/api/shorten`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { UrlState } from './types';
|
|||||||
export const shortenUrl = createAsyncThunk(
|
export const shortenUrl = createAsyncThunk(
|
||||||
'url/shortenUrl',
|
'url/shortenUrl',
|
||||||
async (url: string) => {
|
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 });
|
const data = await shortenUrlApi({ url });
|
||||||
return `${baseUrl}/${data.shortcode}`;
|
return `${baseUrl}/${data.shortcode}`;
|
||||||
}
|
}
|
||||||
|
|||||||
112
generate_configs.py
Normal file
112
generate_configs.py
Normal file
@@ -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")
|
||||||
Reference in New Issue
Block a user