chore(deploy): add production caddy setup
This commit is contained in:
19
.dockerignore
Normal file
19
.dockerignore
Normal file
@@ -0,0 +1,19 @@
|
||||
.git/
|
||||
.mypy_cache/
|
||||
.pytest_cache/
|
||||
.ruff_cache/
|
||||
.venv/
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.egg-info/
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
!.env.production.example
|
||||
htmlcov/
|
||||
build/
|
||||
dist/
|
||||
mongo-data/
|
||||
redis-data/
|
||||
rabbitmq-data/
|
||||
|
||||
24
.env.production.example
Normal file
24
.env.production.example
Normal file
@@ -0,0 +1,24 @@
|
||||
APP_ENV=production
|
||||
MONGO_DB_NAME=gapido_auth
|
||||
JWT_SECRET_KEY=replace-with-a-long-random-production-secret
|
||||
JWT_ISSUER=gapido-auth
|
||||
ACCESS_TOKEN_TTL_SECONDS=900
|
||||
REFRESH_TOKEN_TTL_SECONDS=604800
|
||||
OTP_TTL_SECONDS=120
|
||||
OTP_MAX_ATTEMPTS=5
|
||||
OTP_REQUEST_LIMIT=3
|
||||
OTP_REQUEST_WINDOW_SECONDS=300
|
||||
|
||||
# Choose exactly one real provider in production: kavenegar or sms_ir.
|
||||
SMS_PROVIDER=sms_ir
|
||||
KAVENEGAR_API_KEY=
|
||||
KAVENEGAR_LOGIN_TEMPLATE=login-otp
|
||||
SMS_IR_API_KEY=replace-with-real-sms-ir-key
|
||||
SMS_IR_VERIFY_TEMPLATE_ID=570574
|
||||
|
||||
ADMIN_MOBILE=989120000000
|
||||
DEMO_ENABLE_DEBUG_OTP=false
|
||||
DEMO_DEBUG_SMS_TTL_SECONDS=300
|
||||
GRPC_HOST=0.0.0.0
|
||||
GRPC_PORT=50051
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -24,6 +24,7 @@ htmlcov/
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
!.env.production.example
|
||||
|
||||
# IDE and OS
|
||||
.idea/
|
||||
@@ -37,4 +38,3 @@ Thumbs.db
|
||||
mongo-data/
|
||||
redis-data/
|
||||
rabbitmq-data/
|
||||
|
||||
|
||||
@@ -8,14 +8,16 @@ WORKDIR /app
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends curl \
|
||||
&& groupadd --system app \
|
||||
&& useradd --system --gid app --home-dir /app app \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY pyproject.toml README.md ./
|
||||
COPY src ./src
|
||||
COPY proto ./proto
|
||||
|
||||
RUN pip install --no-cache-dir ".[dev]" \
|
||||
&& python -m gapido_auth.tools.generate_proto
|
||||
RUN pip install --no-cache-dir "."
|
||||
|
||||
USER app
|
||||
|
||||
CMD ["python", "-m", "gapido_auth.transport.grpc.server"]
|
||||
|
||||
|
||||
4
Makefile
4
Makefile
@@ -13,8 +13,10 @@ typecheck:
|
||||
mypy
|
||||
|
||||
compose-up:
|
||||
docker compose up --build
|
||||
docker compose -f docker-compose.yml -f docker-compose.local.yml up --build
|
||||
|
||||
compose-down:
|
||||
docker compose down --remove-orphans
|
||||
|
||||
compose-prod-config:
|
||||
docker compose --env-file .env.production -f docker-compose.yml -f docker-compose.prod.yml config
|
||||
|
||||
22
README.md
22
README.md
@@ -14,11 +14,29 @@ Python gRPC OTP authentication service with MongoDB, Redis, RabbitMQ, and select
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
docker compose up --build
|
||||
docker compose -f docker-compose.yml -f docker-compose.local.yml up --build
|
||||
```
|
||||
|
||||
The gRPC service listens on `localhost:50051`. The demo UI is available at `http://localhost:8080`. RabbitMQ management is available at `http://localhost:15672` with `guest` / `guest`.
|
||||
|
||||
## Production Deployment
|
||||
|
||||
Production uses Caddy as the only public entrypoint for `https://gapido.amiirkhl.ir`.
|
||||
|
||||
```bash
|
||||
cp .env.production.example .env.production
|
||||
docker compose --env-file .env.production -f docker-compose.yml -f docker-compose.prod.yml up -d --build
|
||||
```
|
||||
|
||||
Before running production, set real values in `.env.production`:
|
||||
|
||||
- `JWT_SECRET_KEY`
|
||||
- `ADMIN_MOBILE`
|
||||
- `SMS_PROVIDER`
|
||||
- the selected SMS provider credentials
|
||||
|
||||
Only ports `80` and `443` are published in the production Compose overlay. MongoDB, Redis, RabbitMQ, gRPC, and the FastAPI demo service stay private on the Docker network.
|
||||
|
||||
## SMS Provider
|
||||
|
||||
The SMS integration uses the Strategy pattern behind the `SmsClient` port. Select the provider with:
|
||||
@@ -53,6 +71,8 @@ python -m gapido_auth.tools.generate_proto
|
||||
pytest
|
||||
```
|
||||
|
||||
The production Docker image installs runtime dependencies only. Development tools are installed locally through `.[dev]`.
|
||||
|
||||
## gRPC Methods
|
||||
|
||||
- `RequestOtp`: public; creates a short-lived OTP and publishes an SMS job.
|
||||
|
||||
15
deploy/caddy/Caddyfile
Normal file
15
deploy/caddy/Caddyfile
Normal file
@@ -0,0 +1,15 @@
|
||||
gapido.amiirkhl.ir {
|
||||
encode zstd gzip
|
||||
|
||||
header {
|
||||
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
|
||||
X-Content-Type-Options "nosniff"
|
||||
X-Frame-Options "DENY"
|
||||
Referrer-Policy "strict-origin-when-cross-origin"
|
||||
Permissions-Policy "camera=(), microphone=(), geolocation=()"
|
||||
-Server
|
||||
}
|
||||
|
||||
reverse_proxy demo-app:8080
|
||||
}
|
||||
|
||||
24
docker-compose.local.yml
Normal file
24
docker-compose.local.yml
Normal file
@@ -0,0 +1,24 @@
|
||||
services:
|
||||
auth-service:
|
||||
ports:
|
||||
- "50051:50051"
|
||||
|
||||
demo-app:
|
||||
ports:
|
||||
- "8080:8080"
|
||||
environment:
|
||||
DEMO_ENABLE_DEBUG_OTP: "true"
|
||||
|
||||
mongo:
|
||||
ports:
|
||||
- "27017:27017"
|
||||
|
||||
redis:
|
||||
ports:
|
||||
- "6379:6379"
|
||||
|
||||
rabbitmq:
|
||||
ports:
|
||||
- "5672:5672"
|
||||
- "15672:15672"
|
||||
|
||||
44
docker-compose.prod.yml
Normal file
44
docker-compose.prod.yml
Normal file
@@ -0,0 +1,44 @@
|
||||
services:
|
||||
auth-service:
|
||||
environment:
|
||||
SMS_PROVIDER: ${SMS_PROVIDER:?set SMS_PROVIDER in production env}
|
||||
JWT_SECRET_KEY: ${JWT_SECRET_KEY:?set JWT_SECRET_KEY in production env}
|
||||
ADMIN_MOBILE: ${ADMIN_MOBILE:?set ADMIN_MOBILE in production env}
|
||||
|
||||
sms-worker:
|
||||
environment:
|
||||
SMS_PROVIDER: ${SMS_PROVIDER:?set SMS_PROVIDER in production env}
|
||||
KAVENEGAR_API_KEY: ${KAVENEGAR_API_KEY:-}
|
||||
SMS_IR_API_KEY: ${SMS_IR_API_KEY:-}
|
||||
|
||||
demo-app:
|
||||
environment:
|
||||
DEMO_ENABLE_DEBUG_OTP: "false"
|
||||
depends_on:
|
||||
auth-service:
|
||||
condition: service_healthy
|
||||
|
||||
caddy:
|
||||
image: caddy:2.8-alpine
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
- "443:443/udp"
|
||||
volumes:
|
||||
- ./deploy/caddy/Caddyfile:/etc/caddy/Caddyfile:ro
|
||||
- caddy-data:/data
|
||||
- caddy-config:/config
|
||||
depends_on:
|
||||
demo-app:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD", "caddy", "validate", "--config", "/etc/caddy/Caddyfile"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
volumes:
|
||||
caddy-data:
|
||||
caddy-config:
|
||||
|
||||
@@ -21,8 +21,18 @@ services:
|
||||
ADMIN_MOBILE: ${ADMIN_MOBILE:-989120000000}
|
||||
GRPC_HOST: 0.0.0.0
|
||||
GRPC_PORT: 50051
|
||||
ports:
|
||||
- "50051:50051"
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD",
|
||||
"python",
|
||||
"-c",
|
||||
"import grpc; from grpc_health.v1 import health_pb2, health_pb2_grpc; channel=grpc.insecure_channel('localhost:50051'); stub=health_pb2_grpc.HealthStub(channel); stub.Check(health_pb2.HealthCheckRequest(), timeout=3)",
|
||||
]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 12
|
||||
start_period: 20s
|
||||
depends_on:
|
||||
mongo:
|
||||
condition: service_started
|
||||
@@ -51,32 +61,31 @@ services:
|
||||
environment:
|
||||
AUTH_GRPC_TARGET: auth-service:50051
|
||||
REDIS_URL: redis://redis:6379/0
|
||||
DEMO_ENABLE_DEBUG_OTP: "true"
|
||||
ports:
|
||||
- "8080:8080"
|
||||
DEMO_ENABLE_DEBUG_OTP: ${DEMO_ENABLE_DEBUG_OTP:-true}
|
||||
healthcheck:
|
||||
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8080/healthz', timeout=3).read()"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 12
|
||||
start_period: 10s
|
||||
depends_on:
|
||||
auth-service:
|
||||
condition: service_started
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_started
|
||||
|
||||
mongo:
|
||||
image: mongo:7
|
||||
ports:
|
||||
- "27017:27017"
|
||||
volumes:
|
||||
- mongo-data:/data/db
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
ports:
|
||||
- "6379:6379"
|
||||
|
||||
rabbitmq:
|
||||
image: rabbitmq:3.13-management-alpine
|
||||
ports:
|
||||
- "5672:5672"
|
||||
- "15672:15672"
|
||||
volumes:
|
||||
- rabbitmq-data:/var/lib/rabbitmq
|
||||
healthcheck:
|
||||
test: ["CMD", "rabbitmq-diagnostics", "check_port_connectivity"]
|
||||
interval: 5s
|
||||
@@ -85,3 +94,4 @@ services:
|
||||
|
||||
volumes:
|
||||
mongo-data:
|
||||
rabbitmq-data:
|
||||
|
||||
@@ -9,7 +9,6 @@ dependencies = [
|
||||
"grpcio==1.68.1",
|
||||
"grpcio-health-checking==1.68.1",
|
||||
"grpcio-reflection==1.68.1",
|
||||
"grpcio-tools==1.68.1",
|
||||
"httpx==0.28.1",
|
||||
"motor==3.6.0",
|
||||
"protobuf==5.29.2",
|
||||
@@ -22,6 +21,7 @@ dependencies = [
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"grpcio-tools==1.68.1",
|
||||
"mypy==1.14.1",
|
||||
"pytest==8.3.4",
|
||||
"pytest-asyncio==0.25.2",
|
||||
|
||||
@@ -97,6 +97,11 @@ async def index() -> FileResponse:
|
||||
return FileResponse(STATIC_DIR / "index.html")
|
||||
|
||||
|
||||
@app.get("/healthz")
|
||||
async def healthz() -> dict[str, str]:
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@app.post("/api/auth/request-otp")
|
||||
async def request_otp(
|
||||
body: RequestOtpBody,
|
||||
|
||||
@@ -105,6 +105,16 @@ def test_demo_auth_flow_forwards_to_grpc_client() -> None:
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
|
||||
def test_healthz() -> None:
|
||||
client = TestClient(app)
|
||||
try:
|
||||
response = client.get("/healthz")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"status": "ok"}
|
||||
finally:
|
||||
client.close()
|
||||
|
||||
|
||||
def test_debug_otp_endpoint_reads_redis_when_enabled() -> None:
|
||||
app.dependency_overrides[get_app_settings] = lambda: Settings(demo_enable_debug_otp=True)
|
||||
app.dependency_overrides[get_debug_redis] = lambda: FakeDebugRedis()
|
||||
|
||||
Reference in New Issue
Block a user