chore: initial hub repo structure
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
# Infrastructure
|
||||
|
||||
Docker Compose and Traefik configuration for `asepharyana-hub`.
|
||||
|
||||
## Layout
|
||||
|
||||
```text
|
||||
infra/
|
||||
├── compose/ # One compose file per stack/service
|
||||
│ ├── traefik.yml # Public reverse proxy
|
||||
│ ├── shared.yml # Shared Redis
|
||||
│ ├── react.yml # React SPA
|
||||
│ ├── scraper.yml # Scraper API
|
||||
│ ├── elysia.yml # Elysia API
|
||||
│ └── rust-auth.yml # Rust auth API
|
||||
├── docker/ # Dockerfiles and image runtime helpers
|
||||
├── traefik/ # Static and dynamic Traefik configuration
|
||||
│ ├── dynamic/ # Routers, services, middlewares, TLS certs
|
||||
│ └── TRAEFIK_ENV_CONFIG.md
|
||||
└── config/ # Service bootstrap configuration
|
||||
```
|
||||
|
||||
Archived configs that are not deployed live under `docs/config/`.
|
||||
|
||||
## First-time setup
|
||||
|
||||
Create the shared Docker network before starting any service:
|
||||
|
||||
```bash
|
||||
docker network create app-shared-net
|
||||
```
|
||||
|
||||
Create `.env` from `.env.example` and fill production values. Do not commit `.env`.
|
||||
|
||||
## Deployment order
|
||||
|
||||
The GitHub deploy workflow combines the active compose files automatically. For manual deployment, use this order:
|
||||
|
||||
```bash
|
||||
docker compose -f infra/compose/shared.yml up -d
|
||||
docker compose -f infra/compose/traefik.yml up -d
|
||||
|
||||
docker compose \
|
||||
-f infra/compose/react.yml \
|
||||
-f infra/compose/scraper.yml \
|
||||
-f infra/compose/elysia.yml \
|
||||
-f infra/compose/rust-auth.yml \
|
||||
up -d
|
||||
```
|
||||
|
||||
## Environment variables
|
||||
|
||||
Common variables used by infra compose files:
|
||||
|
||||
```env
|
||||
DATABASE_URL=
|
||||
GITHUB_TOKEN=
|
||||
JWT_SECRET=
|
||||
SHARED_REDIS_EXPOSE=127.0.0.1:6379:6379
|
||||
```
|
||||
|
||||
Traefik certificate path variables are optional because `infra/compose/traefik.yml` provides production-compatible defaults. See `infra/traefik/TRAEFIK_ENV_CONFIG.md` for the full list.
|
||||
|
||||
## Traefik
|
||||
|
||||
Traefik reads dynamic config from `infra/traefik/dynamic/`:
|
||||
|
||||
- `apps.yaml` — routers and upstream services
|
||||
- `middlewares.yaml` — shared middleware chains
|
||||
- `ssl.yaml` — TLS certificates
|
||||
|
||||
The primary certificate intentionally pairs `asephstech.pem` with `asephscloud.key` to preserve the current production layout.
|
||||
|
||||
## Validation
|
||||
|
||||
Run syntax checks after editing infra YAML:
|
||||
|
||||
```bash
|
||||
python - <<'PY'
|
||||
import pathlib, yaml
|
||||
for path in pathlib.Path('infra').rglob('*.yml'):
|
||||
with path.open() as fh:
|
||||
yaml.safe_load(fh)
|
||||
print(f'OK {path}')
|
||||
for path in pathlib.Path('infra').rglob('*.yaml'):
|
||||
with path.open() as fh:
|
||||
yaml.safe_load(fh)
|
||||
print(f'OK {path}')
|
||||
PY
|
||||
```
|
||||
|
||||
Check compose rendering when Docker is available:
|
||||
|
||||
```bash
|
||||
for f in infra/compose/*.yml; do
|
||||
docker compose -f "$f" config >/dev/null && echo "OK $f"
|
||||
done
|
||||
```
|
||||
@@ -0,0 +1,24 @@
|
||||
services:
|
||||
elysia-api:
|
||||
container_name: elysia-api
|
||||
image: ghcr.io/asepharyana/asepharyana-hub/elysia-api:sha-0899edc
|
||||
restart: always
|
||||
networks:
|
||||
app-shared-net:
|
||||
aliases:
|
||||
- elysia-api
|
||||
env_file:
|
||||
- ../../.env
|
||||
environment:
|
||||
- REDIS_URL=redis://redis:6379
|
||||
- JWT_SECRET=${JWT_SECRET:?JWT_SECRET is required}
|
||||
- DATABASE_URL=${DATABASE_URL}
|
||||
- GITHUB_TOKEN=${GITHUB_TOKEN}
|
||||
- PORT=4092
|
||||
- OTEL_EXPORTER_OTLP_ENDPOINT=http://otlp-metrics-backend:4317
|
||||
- OTEL_SERVICE_NAME=elysia-api
|
||||
|
||||
networks:
|
||||
app-shared-net:
|
||||
name: app-shared-net
|
||||
external: true
|
||||
@@ -0,0 +1,13 @@
|
||||
services:
|
||||
react-web:
|
||||
container_name: react-web
|
||||
image: ghcr.io/asepharyana/asepharyana-hub/react-web:sha-0899edc
|
||||
restart: always
|
||||
networks: [app-shared-net]
|
||||
environment:
|
||||
- VITE_API_URL=https://scraper.asepharyana.my.id/api
|
||||
- VITE_ELYSIA_URL=https://elysia.asepharyana.my.id
|
||||
networks:
|
||||
app-shared-net:
|
||||
name: app-shared-net
|
||||
external: true
|
||||
@@ -0,0 +1,20 @@
|
||||
services:
|
||||
rust-auth:
|
||||
container_name: rust-auth-api
|
||||
image: ghcr.io/asepharyana/asepharyana-hub/rust-auth:sha-0899edc
|
||||
restart: always
|
||||
networks:
|
||||
app-shared-net:
|
||||
aliases:
|
||||
- rust-auth
|
||||
env_file:
|
||||
- ../../.env
|
||||
environment:
|
||||
- PORT=3000
|
||||
- OTEL_EXPORTER_OTLP_ENDPOINT=http://otlp-metrics-backend:4317
|
||||
- OTEL_SERVICE_NAME=rust-auth
|
||||
|
||||
networks:
|
||||
app-shared-net:
|
||||
name: app-shared-net
|
||||
external: true
|
||||
@@ -0,0 +1,21 @@
|
||||
services:
|
||||
scraper-api:
|
||||
container_name: scraper-api
|
||||
image: ghcr.io/asepharyana/asepharyana-hub/scraper-api:sha-0899edc
|
||||
restart: always
|
||||
networks:
|
||||
app-shared-net:
|
||||
aliases:
|
||||
- scraper-api
|
||||
env_file:
|
||||
- ../../.env
|
||||
environment:
|
||||
- REDIS_URL=redis://redis:6379
|
||||
- JWT_SECRET=${JWT_SECRET:?JWT_SECRET is required}
|
||||
- DATABASE_URL=${DATABASE_URL}
|
||||
- OTEL_EXPORTER_OTLP_ENDPOINT=http://otlp-metrics-backend:4317
|
||||
- OTEL_SERVICE_NAME=scraper-api
|
||||
networks:
|
||||
app-shared-net:
|
||||
name: app-shared-net
|
||||
external: true
|
||||
@@ -0,0 +1,21 @@
|
||||
services:
|
||||
redis:
|
||||
container_name: redis
|
||||
image: 'redis:alpine'
|
||||
restart: always
|
||||
networks:
|
||||
app-shared-net:
|
||||
aliases:
|
||||
- redis
|
||||
ports:
|
||||
- '${SHARED_REDIS_EXPOSE:-127.0.0.1:6379:6379}'
|
||||
volumes:
|
||||
- 'redis_data:/data'
|
||||
|
||||
networks:
|
||||
app-shared-net:
|
||||
name: app-shared-net
|
||||
external: true
|
||||
|
||||
volumes:
|
||||
redis_data: null
|
||||
@@ -0,0 +1,74 @@
|
||||
services:
|
||||
traefik:
|
||||
container_name: traefik
|
||||
image: traefik:v3.6
|
||||
restart: always
|
||||
sysctls:
|
||||
- net.core.somaxconn=65535
|
||||
- net.ipv4.ip_local_port_range=1024 65535
|
||||
ulimits:
|
||||
nofile:
|
||||
soft: 1048576
|
||||
hard: 1048576
|
||||
ports:
|
||||
- '80:80'
|
||||
- '443:443'
|
||||
networks:
|
||||
- app-shared-net
|
||||
extra_hosts:
|
||||
- 'host.docker.internal:10.0.1.1'
|
||||
command:
|
||||
- '--api.dashboard=true'
|
||||
- '--api.insecure=false'
|
||||
- '--providers.docker=true'
|
||||
- '--providers.docker.endpoint=unix:///var/run/docker.sock'
|
||||
- '--providers.docker.exposedByDefault=false'
|
||||
- '--providers.docker.network=app-shared-net'
|
||||
- '--providers.docker.watch=true'
|
||||
- '--providers.file.directory=/etc/traefik/dynamic'
|
||||
- '--providers.file.watch=true'
|
||||
- '--entryPoints.web.address=:80'
|
||||
- '--entryPoints.web.http.redirections.entryPoint.to=websecure'
|
||||
- '--entryPoints.web.http.redirections.entryPoint.scheme=https'
|
||||
- '--accesslog=true'
|
||||
- '--accesslog.bufferingsize=100'
|
||||
- '--log.level=INFO'
|
||||
- '--log.format=json'
|
||||
- '--entryPoints.web.transport.respondingTimeouts.readTimeout=0'
|
||||
- '--entryPoints.web.transport.respondingTimeouts.writeTimeout=0'
|
||||
- '--entryPoints.web.transport.respondingTimeouts.idleTimeout=0'
|
||||
- '--entryPoints.web.transport.lifeCycle.requestAcceptGraceTimeout=15s'
|
||||
- '--entryPoints.web.transport.lifeCycle.graceTimeOut=10s'
|
||||
- '--entryPoints.websecure.transport.respondingTimeouts.readTimeout=0'
|
||||
- '--entryPoints.websecure.transport.respondingTimeouts.writeTimeout=0'
|
||||
- '--entryPoints.websecure.transport.respondingTimeouts.idleTimeout=0'
|
||||
- '--entryPoints.websecure.transport.lifeCycle.requestAcceptGraceTimeout=15s'
|
||||
- '--entryPoints.websecure.transport.lifeCycle.graceTimeOut=10s'
|
||||
- '--entryPoints.websecure.address=:443'
|
||||
- '--experimental.plugins.real-ip.moduleName=github.com/soulbalz/traefik-real-ip'
|
||||
- '--experimental.plugins.real-ip.version=v1.0.3'
|
||||
- '--experimental.plugins.blockpath.moduleName=github.com/traefik/plugin-blockpath'
|
||||
- '--experimental.plugins.blockpath.version=v0.2.1'
|
||||
environment:
|
||||
- DOCKER_API_VERSION=1.41
|
||||
- GOMEMLIMIT=4096MiB
|
||||
- GOGC=200
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
- ${TRAEFIK_CONFIG_PATH:-/root/asepharyana-hub/infra/traefik/dynamic}:/etc/traefik/dynamic:ro
|
||||
- ${TRAEFIK_CERT_ASEPHARYANA_MY_ID_PEM:-/root/asepharyana.my.id.pem}:/etc/traefik/certs/asepharyana.my.id.pem:ro
|
||||
- ${TRAEFIK_CERT_ASEPHARYANA_MY_ID_KEY:-/root/asepharyana.my.id.key}:/etc/traefik/certs/asepharyana.my.id.key:ro
|
||||
- ${TRAEFIK_CERT_ASEPHARYANA_WEB_ID_PEM:-/root/asepharyana.web.id.pem}:/etc/traefik/certs/asepharyana.web.id.pem:ro
|
||||
- ${TRAEFIK_CERT_ASEPHARYANA_WEB_ID_KEY:-/root/asepharyana.web.id.key}:/etc/traefik/certs/asepharyana.web.id.key:ro
|
||||
labels:
|
||||
- 'traefik.enable=true'
|
||||
- 'traefik.http.routers.traefik.rule=Host(`traefik.asepharyana.my.id`) || Host(`traefik.asepharyana.web.id`)'
|
||||
- 'traefik.http.routers.traefik.service=api@internal'
|
||||
- 'traefik.http.routers.traefik.entrypoints=websecure'
|
||||
- 'traefik.http.routers.traefik.tls=true'
|
||||
- 'traefik.http.routers.traefik.middlewares=admin-chain@file'
|
||||
|
||||
networks:
|
||||
app-shared-net:
|
||||
name: app-shared-net
|
||||
external: true
|
||||
@@ -0,0 +1,10 @@
|
||||
-- Create database if not exists
|
||||
CREATE DATABASE IF NOT EXISTS `tracer_study`;
|
||||
|
||||
-- Create dedicated user for tracer_study
|
||||
CREATE USER IF NOT EXISTS 'tracerstudy'@'%' IDENTIFIED BY 'tracerstudy_secret';
|
||||
GRANT ALL PRIVILEGES ON `tracer_study`.* TO 'tracerstudy'@'%';
|
||||
|
||||
|
||||
-- Flush privileges
|
||||
FLUSH PRIVILEGES;
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,26 @@
|
||||
# build stage
|
||||
FROM oven/bun:1-alpine AS builder
|
||||
WORKDIR /app
|
||||
|
||||
# install dependencies with cache mounts
|
||||
COPY apps/elysia/package.json apps/elysia/bun.lock ./
|
||||
RUN --mount=type=cache,target=/root/.bun/install/cache \
|
||||
bun install --frozen-lockfile
|
||||
|
||||
# build the application
|
||||
COPY apps/elysia ./
|
||||
RUN bun run build
|
||||
|
||||
# runtime stage
|
||||
FROM oven/bun:1-distroless
|
||||
WORKDIR /app
|
||||
|
||||
# copy build artifacts
|
||||
COPY --from=builder /app/dist ./dist
|
||||
COPY --from=builder /app/package.json ./
|
||||
|
||||
# distroless uses nonroot user (UID 65532) by default, or we can use it
|
||||
USER nonroot
|
||||
|
||||
EXPOSE 4092
|
||||
CMD ["run", "dist/index.js"]
|
||||
Vendored
+75
@@ -0,0 +1,75 @@
|
||||
import { existsSync, readFileSync } from "node:fs"
|
||||
import { resolve, sep } from "node:path"
|
||||
|
||||
const distDir = resolve("./dist")
|
||||
const indexPath = resolve(distDir, "index.html")
|
||||
|
||||
const contentTypes = {
|
||||
html: "text/html; charset=utf-8",
|
||||
css: "text/css; charset=utf-8",
|
||||
js: "application/javascript; charset=utf-8",
|
||||
json: "application/json; charset=utf-8",
|
||||
svg: "image/svg+xml",
|
||||
png: "image/png",
|
||||
jpg: "image/jpeg",
|
||||
jpeg: "image/jpeg",
|
||||
gif: "image/gif",
|
||||
webp: "image/webp",
|
||||
ico: "image/x-icon",
|
||||
woff: "font/woff",
|
||||
woff2: "font/woff2",
|
||||
ttf: "font/ttf",
|
||||
}
|
||||
|
||||
function responseFromFile(filePath, headers) {
|
||||
try {
|
||||
return new Response(readFileSync(filePath), { headers })
|
||||
} catch (error) {
|
||||
if (error?.code === "ENOENT") {
|
||||
return new Response("Not Found", { status: 404 })
|
||||
}
|
||||
|
||||
console.error("File read error:", error)
|
||||
return new Response("Internal Server Error", { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
function serveIndex() {
|
||||
return responseFromFile(indexPath, {
|
||||
"Content-Type": contentTypes.html,
|
||||
"Cache-Control": "no-cache",
|
||||
})
|
||||
}
|
||||
|
||||
function serveFile(filePath, pathname) {
|
||||
const ext = filePath.split(".").pop() || ""
|
||||
return responseFromFile(filePath, {
|
||||
"Content-Type": contentTypes[ext] || "application/octet-stream",
|
||||
"Cache-Control": pathname.startsWith("/assets/") ? "public, max-age=31536000, immutable" : "no-cache",
|
||||
})
|
||||
}
|
||||
|
||||
Bun.serve({
|
||||
port: 80,
|
||||
hostname: "0.0.0.0",
|
||||
fetch(req) {
|
||||
const url = new URL(req.url)
|
||||
const pathname = url.pathname
|
||||
const filePath = resolve(distDir, pathname.slice(1))
|
||||
const insideDist = filePath === distDir || filePath.startsWith(`${distDir}${sep}`)
|
||||
|
||||
if (!insideDist) {
|
||||
return new Response("Forbidden", { status: 403 })
|
||||
}
|
||||
|
||||
if (pathname !== "/" && existsSync(filePath)) {
|
||||
return serveFile(filePath, pathname)
|
||||
}
|
||||
|
||||
if (!pathname.includes(".") && existsSync(indexPath)) {
|
||||
return serveIndex()
|
||||
}
|
||||
|
||||
return new Response("Not Found", { status: 404 })
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,16 @@
|
||||
# ─── Stage 1: Build ─────────────────────────────────────────────────────────
|
||||
FROM oven/bun:1 AS builder
|
||||
WORKDIR /app
|
||||
COPY apps/react/package.json apps/react/bun.lock ./
|
||||
RUN bun install --frozen-lockfile
|
||||
COPY apps/react .
|
||||
RUN bun run build
|
||||
|
||||
# ─── Stage 2: Runtime (Bun static server) ──────────────────────────────────
|
||||
FROM oven/bun:1-alpine
|
||||
WORKDIR /app
|
||||
COPY infra/docker/react-server.js ./server.js
|
||||
COPY --from=builder /app/dist ./dist
|
||||
|
||||
EXPOSE 80
|
||||
CMD ["bun", "server.js"]
|
||||
@@ -0,0 +1,42 @@
|
||||
# Use cargo-chef for dependency caching
|
||||
FROM lukemathwalker/cargo-chef:latest-rust-1.89.0 AS chef
|
||||
WORKDIR /app
|
||||
|
||||
FROM chef AS planner
|
||||
COPY apps/rust-auth .
|
||||
RUN cargo chef prepare --recipe-path recipe.json
|
||||
|
||||
FROM chef AS builder
|
||||
COPY --from=planner /app/recipe.json recipe.json
|
||||
# Utilize buildkit cache mounts for cargo
|
||||
RUN --mount=type=cache,target=/usr/local/cargo/registry \
|
||||
--mount=type=cache,target=/app/target \
|
||||
cargo chef cook --release --recipe-path recipe.json
|
||||
|
||||
# Build application
|
||||
COPY apps/rust-auth .
|
||||
RUN --mount=type=cache,target=/usr/local/cargo/registry \
|
||||
--mount=type=cache,target=/app/target \
|
||||
cargo build --release && \
|
||||
cp target/release/rust-auth /app/rust-auth
|
||||
|
||||
# Final runtime image
|
||||
FROM debian:bookworm-slim AS runtime
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
curl \
|
||||
libssl3 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Add non-root user
|
||||
RUN groupadd -g 1001 appgroup && \
|
||||
useradd -u 1001 -g appgroup -s /bin/sh appuser
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/rust-auth /app/rust-auth
|
||||
|
||||
# Run as non-root
|
||||
USER appuser
|
||||
|
||||
EXPOSE 3000
|
||||
CMD ["./rust-auth"]
|
||||
@@ -0,0 +1,50 @@
|
||||
# Use cargo-chef for dependency caching
|
||||
FROM lukemathwalker/cargo-chef:latest-rust-1.89.0 AS chef
|
||||
WORKDIR /app
|
||||
|
||||
# Install Node.js if needed for build scripts
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
nodejs \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
FROM chef AS planner
|
||||
COPY apps/scraper .
|
||||
RUN cargo chef prepare --recipe-path recipe.json
|
||||
|
||||
FROM chef AS builder
|
||||
COPY --from=planner /app/recipe.json recipe.json
|
||||
# Utilize buildkit cache mounts for cargo
|
||||
RUN --mount=type=cache,target=/usr/local/cargo/registry \
|
||||
--mount=type=cache,target=/app/target \
|
||||
cargo chef cook --release --recipe-path recipe.json
|
||||
|
||||
# Build application
|
||||
COPY apps/scraper .
|
||||
RUN --mount=type=cache,target=/usr/local/cargo/registry \
|
||||
--mount=type=cache,target=/app/target \
|
||||
cargo build --release && \
|
||||
cp target/release/scraper /app/scraper
|
||||
|
||||
# Final runtime image
|
||||
FROM debian:bookworm-slim AS runtime
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
curl \
|
||||
libssl3 \
|
||||
chromium \
|
||||
fonts-liberation \
|
||||
fonts-noto-color-emoji \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Add non-root user
|
||||
RUN groupadd -g 1001 appgroup && \
|
||||
useradd -u 1001 -g appgroup -s /bin/sh appuser
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/scraper /app/scraper
|
||||
|
||||
# Run as non-root
|
||||
USER appuser
|
||||
|
||||
EXPOSE 4091
|
||||
CMD ["./scraper"]
|
||||
@@ -0,0 +1,61 @@
|
||||
# Traefik Environment Configuration
|
||||
|
||||
This document describes environment variables used to configure Traefik certificate and config paths in production deployments.
|
||||
|
||||
## Certificate Path Environment Variables
|
||||
|
||||
All certificate paths support environment variable substitution with safe fallback defaults. This allows flexible certificate management across different deployment environments without modifying compose files.
|
||||
|
||||
### Configuration Variables
|
||||
|
||||
| Variable | Description | Default Path | Purpose |
|
||||
| ------------------------------------- | --------------------------------------------------------------- | --------------------------------------------- | ------------------------------------------------------- |
|
||||
| `TRAEFIK_CONFIG_PATH` | Directory containing dynamic Traefik configuration files (YAML) | `/root/asepharyana-hub/infra/traefik/dynamic` | Location of middleware, router, and service definitions |
|
||||
| `TRAEFIK_CERT_ASEPHARYANA_MY_ID_PEM` | Certificate file for asepharyana.my.id | `/root/asepharyana.my.id.pem` | SSL/TLS certificate for asepharyana.my.id domain |
|
||||
| `TRAEFIK_CERT_ASEPHARYANA_MY_ID_KEY` | Key file for asepharyana.my.id | `/root/asepharyana.my.id.key` | SSL/TLS private key for asepharyana.my.id domain |
|
||||
| `TRAEFIK_CERT_ASEPHARYANA_WEB_ID_PEM` | Certificate file for asepharyana.web.id | `/root/asepharyana.web.id.pem` | SSL/TLS certificate for asepharyana.web.id domain |
|
||||
| `TRAEFIK_CERT_ASEPHARYANA_WEB_ID_KEY` | Key file for asepharyana.web.id | `/root/asepharyana.web.id.key` | SSL/TLS private key for asepharyana.web.id domain |
|
||||
|
||||
## Usage
|
||||
|
||||
### Default Behavior (Production)
|
||||
|
||||
If no environment variables are set, Traefik will use the default paths shown above. This is suitable for production deployments where certificates are installed at these standard locations.
|
||||
|
||||
```bash
|
||||
docker compose -f infra/compose/traefik.yml up -d
|
||||
```
|
||||
|
||||
### Custom Paths (Custom Deployments)
|
||||
|
||||
To override paths for a custom deployment, set environment variables before starting services:
|
||||
|
||||
```bash
|
||||
export TRAEFIK_CONFIG_PATH=/etc/traefik/custom-dynamic
|
||||
|
||||
docker compose -f infra/compose/traefik.yml up -d
|
||||
```
|
||||
|
||||
### Via .env File
|
||||
|
||||
Create or update your `.env` file in the deployment directory:
|
||||
|
||||
```env
|
||||
TRAEFIK_CONFIG_PATH=/root/asepharyana-hub/infra/traefik/dynamic
|
||||
TRAEFIK_CERT_ASEPHARYANA_MY_ID_PEM=/root/asepharyana.my.id.pem
|
||||
TRAEFIK_CERT_ASEPHARYANA_MY_ID_KEY=/root/asepharyana.my.id.key
|
||||
TRAEFIK_CERT_ASEPHARYANA_WEB_ID_PEM=/root/asepharyana.web.id.pem
|
||||
TRAEFIK_CERT_ASEPHARYANA_WEB_ID_KEY=/root/asepharyana.web.id.key
|
||||
```
|
||||
|
||||
Then deploy:
|
||||
|
||||
```bash
|
||||
docker compose --env-file .env -f infra/compose/traefik.yml up -d
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- All certificate paths use read-only mounts (`:ro`) for security
|
||||
- If a certificate file is missing at the specified path, Docker volume mounting will fail—ensure certificates exist before starting Traefik
|
||||
- The dynamic configuration directory must contain valid YAML files for Traefik to load properly
|
||||
@@ -0,0 +1,57 @@
|
||||
http:
|
||||
routers:
|
||||
# ── React SPA (domain root) ──
|
||||
react:
|
||||
rule: 'Host(`asepharyana.my.id`) || Host(`asepharyana.web.id`)'
|
||||
entryPoints:
|
||||
- websecure
|
||||
tls: {}
|
||||
service: react-service
|
||||
|
||||
# ── Scraper API ──
|
||||
scraper:
|
||||
rule: 'Host(`scraper.asepharyana.my.id`) || Host(`api.asepharyana.my.id`) || Host(`scraper.asepharyana.web.id`) || Host(`api.asepharyana.web.id`)'
|
||||
entryPoints:
|
||||
- websecure
|
||||
tls: {}
|
||||
middlewares:
|
||||
- common-chain@file
|
||||
service: scraper-service
|
||||
|
||||
# ── Elysia API ──
|
||||
elysia:
|
||||
rule: 'Host(`elysia.asepharyana.my.id`) || Host(`elysia.asepharyana.web.id`)'
|
||||
entryPoints:
|
||||
- websecure
|
||||
tls: {}
|
||||
middlewares:
|
||||
- common-chain@file
|
||||
service: elysia-service
|
||||
|
||||
# ── Rust Auth API ──
|
||||
rust-auth:
|
||||
rule: 'Host(`auth.asepharyana.my.id`) || Host(`auth.asepharyana.web.id`)'
|
||||
entryPoints:
|
||||
- websecure
|
||||
tls: {}
|
||||
middlewares:
|
||||
- common-chain@file
|
||||
service: rust-auth-service
|
||||
|
||||
services:
|
||||
react-service:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: 'http://react-web:80'
|
||||
scraper-service:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: 'http://scraper-api:4091'
|
||||
elysia-service:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: 'http://elysia-api:4092'
|
||||
rust-auth-service:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: 'http://rust-auth:3000'
|
||||
@@ -0,0 +1,74 @@
|
||||
http:
|
||||
middlewares:
|
||||
secure-headers:
|
||||
headers:
|
||||
sslRedirect: true
|
||||
forceSTSHeader: true
|
||||
stsSeconds: 31536000
|
||||
stsIncludeSubdomains: true
|
||||
stsPreload: true
|
||||
frameDeny: true
|
||||
contentTypeNosniff: true
|
||||
browserXSSFilter: true
|
||||
referrerPolicy: 'same-origin'
|
||||
customResponseHeaders:
|
||||
X-Content-Type-Options: 'nosniff'
|
||||
X-Frame-Options: 'DENY'
|
||||
X-XSS-Protection: '1; mode=block'
|
||||
Referrer-Policy: 'same-origin'
|
||||
Permissions-Policy: 'geolocation=(), microphone=(), camera=()'
|
||||
compress:
|
||||
compress:
|
||||
minResponseBodyBytes: 256
|
||||
excludedContentTypes:
|
||||
- 'image/*'
|
||||
- 'application/octet-stream'
|
||||
retry:
|
||||
retry:
|
||||
attempts: 3
|
||||
rate-limit:
|
||||
rateLimit:
|
||||
average: 100
|
||||
burst: 50
|
||||
buffer:
|
||||
buffering:
|
||||
maxRequestBodyBytes: 10485760
|
||||
maxResponseBodyBytes: 10485760
|
||||
memRequestBodyBytes: 1048576
|
||||
memResponseBodyBytes: 1048576
|
||||
admin-chain:
|
||||
chain:
|
||||
middlewares:
|
||||
- secure-headers
|
||||
- compress
|
||||
- retry
|
||||
|
||||
# ── Useful Plugins ──
|
||||
real-ip:
|
||||
plugin:
|
||||
real-ip:
|
||||
excludednetworks:
|
||||
- '127.0.0.1/32'
|
||||
realipheader: 'CF-Connecting-IP'
|
||||
|
||||
block-sensitive-paths:
|
||||
plugin:
|
||||
blockpath:
|
||||
regex:
|
||||
- "^/\\.env"
|
||||
- "^/\\.git"
|
||||
- '^/wp-admin'
|
||||
- "^/wp-login\\.php"
|
||||
- "^/config\\.php"
|
||||
|
||||
# ── Common Chain ──
|
||||
common-chain:
|
||||
chain:
|
||||
middlewares:
|
||||
# - real-ip
|
||||
# - block-sensitive-paths
|
||||
- secure-headers
|
||||
- compress
|
||||
- retry
|
||||
- rate-limit
|
||||
- buffer
|
||||
@@ -0,0 +1,14 @@
|
||||
tls:
|
||||
certificates:
|
||||
# Legacy production layout: asephstech.pem is paired with asephscloud.key.
|
||||
- certFile: /etc/traefik/certs/asephstech.pem
|
||||
keyFile: /etc/traefik/certs/asephscloud.key
|
||||
- certFile: /etc/traefik/certs/asepharyana.my.id.pem
|
||||
keyFile: /etc/traefik/certs/asepharyana.my.id.key
|
||||
- certFile: /etc/traefik/certs/asepharyana.web.id.pem
|
||||
keyFile: /etc/traefik/certs/asepharyana.web.id.key
|
||||
stores:
|
||||
default:
|
||||
defaultCertificate:
|
||||
certFile: /etc/traefik/certs/asephstech.pem
|
||||
keyFile: /etc/traefik/certs/asephscloud.key
|
||||
@@ -0,0 +1,29 @@
|
||||
api:
|
||||
dashboard: true
|
||||
insecure: true
|
||||
|
||||
log:
|
||||
level: INFO
|
||||
format: json
|
||||
|
||||
accessLog: {}
|
||||
|
||||
entryPoints:
|
||||
web:
|
||||
address: ':80'
|
||||
http:
|
||||
redirections:
|
||||
entryPoint:
|
||||
to: websecure
|
||||
scheme: https
|
||||
websecure:
|
||||
address: ':443'
|
||||
|
||||
providers:
|
||||
docker:
|
||||
endpoint: 'unix:///var/run/docker.sock'
|
||||
exposedByDefault: false
|
||||
network: app-shared-net
|
||||
file:
|
||||
directory: /etc/traefik/dynamic
|
||||
watch: true
|
||||
Reference in New Issue
Block a user