first commit
This commit is contained in:
79
deploy/README.md
Normal file
79
deploy/README.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# Production deployment
|
||||
|
||||
This stack exposes only Caddy on ports 80 and 443. PostgreSQL and the FastAPI backend
|
||||
are isolated in the private Docker network. Caddy serves the Vite SPA and proxies public
|
||||
`/api/*` requests to the backend after removing the `/api` prefix.
|
||||
|
||||
## First deployment
|
||||
|
||||
1. Point the `malenia.space` A record to the VPS public IPv4 address.
|
||||
2. Install Docker Engine and the Docker Compose plugin on the VPS. Allow only TCP 22, 80,
|
||||
and 443 through the host firewall.
|
||||
3. Clone the repository to `/opt/malenia` and create `/opt/malenia/.env` from
|
||||
`.env.example`. Set file mode `600` and replace all placeholder values.
|
||||
4. Put `private.pem` and `public.pem` in `/opt/malenia/keys`, set `KEYS_DIR=/opt/malenia/keys`
|
||||
in `.env`, then grant access only to the backend container user:
|
||||
|
||||
```bash
|
||||
sudo chown -R 10001:10001 /opt/malenia/keys
|
||||
sudo chmod 700 /opt/malenia/keys
|
||||
sudo chmod 600 /opt/malenia/keys/*.pem
|
||||
```
|
||||
5. Build and start PostgreSQL, then apply migrations:
|
||||
|
||||
```bash
|
||||
docker compose --env-file .env -f compose.production.yml up -d postgres
|
||||
docker compose --env-file .env -f compose.production.yml --profile tools run --rm migrate
|
||||
```
|
||||
|
||||
6. Start the application:
|
||||
|
||||
```bash
|
||||
docker compose --env-file .env -f compose.production.yml up -d --build
|
||||
```
|
||||
|
||||
7. Verify `https://malenia.space/api/health/` and the primary frontend flows.
|
||||
|
||||
## Releases
|
||||
|
||||
Before each release, run frontend checks locally. On the VPS, pull the intended revision,
|
||||
apply migrations, then rebuild and recreate services:
|
||||
|
||||
```bash
|
||||
git pull --ff-only
|
||||
docker compose --env-file .env -f compose.production.yml --profile tools run --rm migrate
|
||||
docker compose --env-file .env -f compose.production.yml up -d --build
|
||||
```
|
||||
|
||||
The Pally callback URL is `https://malenia.space/api/payments/pally/result`.
|
||||
|
||||
## Local production-like test
|
||||
|
||||
This test uses the same images, API proxying, and rate-limit plugin as production, but
|
||||
serves HTTP on `http://localhost:8080`. It does not need a domain, TLS certificate, or VPS.
|
||||
|
||||
1. Create a local `.env` from `.env.example` and fill the required backend integration
|
||||
settings. `POSTGRES_HOST=postgres`, `PRIVATE_KEY_FP=/run/secrets/keys/private.pem`, and
|
||||
`PUBLIC_KEY_FP=/run/secrets/keys/public.pem` must remain unchanged. The local `keys/`
|
||||
directory must contain both PEM files.
|
||||
2. Start PostgreSQL and apply migrations:
|
||||
|
||||
```bash
|
||||
docker compose --env-file .env -f compose.production.yml -f compose.local.yml up -d postgres
|
||||
docker compose --env-file .env -f compose.production.yml -f compose.local.yml --profile tools run --rm migrate
|
||||
```
|
||||
|
||||
3. Build and run the stack:
|
||||
|
||||
```bash
|
||||
docker compose --env-file .env -f compose.production.yml -f compose.local.yml up -d --build
|
||||
```
|
||||
|
||||
4. Check the frontend at `http://localhost:8080` and the backend through Caddy at
|
||||
`http://localhost:8080/api/health/`.
|
||||
|
||||
5. Stop the local stack while preserving its database volume:
|
||||
|
||||
```bash
|
||||
docker compose --env-file .env -f compose.production.yml -f compose.local.yml down
|
||||
```
|
||||
20
deploy/backend/Dockerfile
Normal file
20
deploy/backend/Dockerfile
Normal file
@@ -0,0 +1,20 @@
|
||||
FROM python:3.13-slim
|
||||
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
PIP_NO_CACHE_DIR=1
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN addgroup --gid 10001 --system app && adduser --uid 10001 --system --ingroup app app
|
||||
|
||||
COPY backend/requirements.txt ./
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY backend/ ./
|
||||
|
||||
USER app
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--proxy-headers", "--forwarded-allow-ips", "*"]
|
||||
70
deploy/caddy/Caddyfile
Normal file
70
deploy/caddy/Caddyfile
Normal file
@@ -0,0 +1,70 @@
|
||||
{
|
||||
admin off
|
||||
email {$ACME_EMAIL}
|
||||
}
|
||||
|
||||
malenia.space {
|
||||
encode zstd gzip
|
||||
|
||||
log {
|
||||
output stdout
|
||||
format json
|
||||
}
|
||||
|
||||
header {
|
||||
Strict-Transport-Security "max-age=31536000; includeSubDomains"
|
||||
X-Content-Type-Options "nosniff"
|
||||
X-Frame-Options "DENY"
|
||||
Referrer-Policy "strict-origin-when-cross-origin"
|
||||
Permissions-Policy "camera=(), geolocation=(), microphone=()"
|
||||
}
|
||||
|
||||
@api path /api/*
|
||||
route @api {
|
||||
rate_limit {
|
||||
zone auth {
|
||||
match {
|
||||
path /api/auth/login /api/auth/signup /api/auth/refresh
|
||||
}
|
||||
key {remote_host}
|
||||
events 10
|
||||
window 1m
|
||||
ipv6_prefix 64
|
||||
}
|
||||
zone checkout {
|
||||
match {
|
||||
path /api/orders/checkout
|
||||
}
|
||||
key {remote_host}
|
||||
events 10
|
||||
window 10m
|
||||
ipv6_prefix 64
|
||||
}
|
||||
zone payment_callback {
|
||||
match {
|
||||
path /api/payments/pally/result
|
||||
}
|
||||
key {remote_host}
|
||||
events 60
|
||||
window 1m
|
||||
ipv6_prefix 64
|
||||
}
|
||||
zone api {
|
||||
match {
|
||||
not path /api/health/
|
||||
}
|
||||
key {remote_host}
|
||||
events 120
|
||||
window 1m
|
||||
ipv6_prefix 64
|
||||
}
|
||||
}
|
||||
|
||||
uri strip_prefix /api
|
||||
reverse_proxy backend:8000
|
||||
}
|
||||
|
||||
root * /srv
|
||||
try_files {path} /index.html
|
||||
file_server
|
||||
}
|
||||
57
deploy/caddy/Caddyfile.local
Normal file
57
deploy/caddy/Caddyfile.local
Normal file
@@ -0,0 +1,57 @@
|
||||
{
|
||||
admin off
|
||||
}
|
||||
|
||||
:80 {
|
||||
encode zstd gzip
|
||||
|
||||
log {
|
||||
output stdout
|
||||
format json
|
||||
}
|
||||
|
||||
@api path /api/*
|
||||
route @api {
|
||||
rate_limit {
|
||||
zone auth {
|
||||
match {
|
||||
path /api/auth/login /api/auth/signup /api/auth/refresh
|
||||
}
|
||||
key {remote_host}
|
||||
events 10
|
||||
window 1m
|
||||
}
|
||||
zone checkout {
|
||||
match {
|
||||
path /api/orders/checkout
|
||||
}
|
||||
key {remote_host}
|
||||
events 10
|
||||
window 10m
|
||||
}
|
||||
zone payment_callback {
|
||||
match {
|
||||
path /api/payments/pally/result
|
||||
}
|
||||
key {remote_host}
|
||||
events 60
|
||||
window 1m
|
||||
}
|
||||
zone api {
|
||||
match {
|
||||
not path /api/health/
|
||||
}
|
||||
key {remote_host}
|
||||
events 120
|
||||
window 1m
|
||||
}
|
||||
}
|
||||
|
||||
uri strip_prefix /api
|
||||
reverse_proxy backend:8000
|
||||
}
|
||||
|
||||
root * /srv
|
||||
try_files {path} /index.html
|
||||
file_server
|
||||
}
|
||||
25
deploy/caddy/Dockerfile
Normal file
25
deploy/caddy/Dockerfile
Normal file
@@ -0,0 +1,25 @@
|
||||
ARG CADDY_VERSION=2.10.2
|
||||
ARG CADDYFILE=deploy/caddy/Caddyfile
|
||||
|
||||
FROM node:22-alpine AS frontend
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY frontend/package.json frontend/package-lock.json ./
|
||||
RUN npm ci
|
||||
|
||||
COPY frontend/ ./
|
||||
RUN npm run build
|
||||
|
||||
FROM caddy:${CADDY_VERSION}-builder AS builder
|
||||
|
||||
RUN xcaddy build \
|
||||
--with github.com/mholt/caddy-ratelimit@5625512f24f6f59d6f64fb3aafe5eecff0b286db
|
||||
|
||||
FROM caddy:${CADDY_VERSION}-alpine
|
||||
|
||||
ARG CADDYFILE
|
||||
|
||||
COPY --from=builder /usr/bin/caddy /usr/bin/caddy
|
||||
COPY ${CADDYFILE} /etc/caddy/Caddyfile
|
||||
COPY --from=frontend /app/dist/ /srv/
|
||||
Reference in New Issue
Block a user