14 changed files with 207 additions and 292 deletions
@ -1,15 +0,0 @@ |
|||||||
# Copy to .env.prod, fill in, then: set -a && source .env.prod && set +a |
|
||||||
# Or use: export $(grep -v '^#' .env.prod | xargs) |
|
||||||
|
|
||||||
POSTGRES_HOST=postgres |
|
||||||
POSTGRES_DB=gc_index_relay_prod |
|
||||||
POSTGRES_USER=postgres |
|
||||||
POSTGRES_PASSWORD=change-me |
|
||||||
POSTGRES_RUNTIME_USER=gc_index_relay |
|
||||||
POSTGRES_RUNTIME_PASSWORD=change-me-runtime |
|
||||||
|
|
||||||
SECRET_KEY_BASE=generate-with-mix-phx-gen-secret |
|
||||||
PHX_HOST=gc-http-relay.imwald.eu |
|
||||||
|
|
||||||
# Optional: pin release image (default latest) |
|
||||||
# TAG=0.2.0 |
|
||||||
@ -1,118 +0,0 @@ |
|||||||
# Production stack: Apache (on the host) terminates TLS and proxies to 127.0.0.1:4000. |
|
||||||
# |
|
||||||
# Helper script: ./scripts/deploy_prod.sh --help |
|
||||||
# |
|
||||||
# --- Local: build and push --- |
|
||||||
# cp .env.prod.example .env.prod && edit secrets |
|
||||||
# export TAG=0.2.0 # optional; relay/migrator use :latest if unset |
|
||||||
# docker login |
|
||||||
# ./scripts/deploy_prod.sh build-push |
|
||||||
# |
|
||||||
# --- Remote: pull and run --- |
|
||||||
# ./scripts/deploy_prod.sh deploy |
|
||||||
# |
|
||||||
# Images (repository: silberengel/gc-http-relay): |
|
||||||
# :${TAG} — Phoenix release (relay + migrator) |
|
||||||
# :setup — one-shot DB user bootstrap (tag is literal "setup") |
|
||||||
|
|
||||||
services: |
|
||||||
postgres: |
|
||||||
image: docker.io/apache/age:release_PG17_1.6.0 |
|
||||||
restart: unless-stopped |
|
||||||
user: 1000:1000 |
|
||||||
volumes: |
|
||||||
- pgdata:/var/lib/postgresql/data |
|
||||||
environment: |
|
||||||
POSTGRES_DB: ${POSTGRES_DB:?set POSTGRES_DB in .env.prod} |
|
||||||
POSTGRES_USER: ${POSTGRES_USER:?set POSTGRES_USER in .env.prod} |
|
||||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env.prod} |
|
||||||
command: > |
|
||||||
postgres |
|
||||||
healthcheck: |
|
||||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"] |
|
||||||
interval: 10s |
|
||||||
timeout: 5s |
|
||||||
retries: 5 |
|
||||||
deploy: |
|
||||||
resources: |
|
||||||
limits: |
|
||||||
cpus: "1.00" |
|
||||||
memory: 1G |
|
||||||
reservations: |
|
||||||
cpus: "0.50" |
|
||||||
memory: 512M |
|
||||||
networks: |
|
||||||
- internal |
|
||||||
|
|
||||||
setup: |
|
||||||
image: docker.io/silberengel/gc-http-relay:setup |
|
||||||
build: |
|
||||||
context: . |
|
||||||
dockerfile: ./docker/setup.Dockerfile |
|
||||||
command: ["/usr/local/bin/usersetup.sh"] |
|
||||||
restart: "no" |
|
||||||
depends_on: |
|
||||||
postgres: |
|
||||||
condition: service_healthy |
|
||||||
environment: |
|
||||||
POSTGRES_HOST: ${POSTGRES_HOST:-postgres} |
|
||||||
POSTGRES_USER: ${POSTGRES_USER:?set POSTGRES_USER in .env.prod} |
|
||||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env.prod} |
|
||||||
POSTGRES_DB: ${POSTGRES_DB:?set POSTGRES_DB in .env.prod} |
|
||||||
POSTGRES_RUNTIME_USER: ${POSTGRES_RUNTIME_USER:?set POSTGRES_RUNTIME_USER in .env.prod} |
|
||||||
POSTGRES_RUNTIME_PASSWORD: ${POSTGRES_RUNTIME_PASSWORD:?set POSTGRES_RUNTIME_PASSWORD in .env.prod} |
|
||||||
networks: |
|
||||||
- internal |
|
||||||
|
|
||||||
migrator: |
|
||||||
image: docker.io/silberengel/gc-http-relay:${TAG:-latest} |
|
||||||
build: |
|
||||||
context: . |
|
||||||
dockerfile: ./docker/server.Dockerfile |
|
||||||
command: ["/app/bin/migrate"] |
|
||||||
restart: "no" |
|
||||||
depends_on: |
|
||||||
postgres: |
|
||||||
condition: service_healthy |
|
||||||
setup: |
|
||||||
condition: service_completed_successfully |
|
||||||
environment: |
|
||||||
DATABASE_URL: "ecto://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST:-postgres}/${POSTGRES_DB}" |
|
||||||
SECRET_KEY_BASE: ${SECRET_KEY_BASE:?set SECRET_KEY_BASE in .env.prod} |
|
||||||
networks: |
|
||||||
- internal |
|
||||||
|
|
||||||
relay: |
|
||||||
image: docker.io/silberengel/gc-http-relay:${TAG:-latest} |
|
||||||
build: |
|
||||||
context: . |
|
||||||
dockerfile: ./docker/server.Dockerfile |
|
||||||
command: ["/app/bin/server"] |
|
||||||
restart: unless-stopped |
|
||||||
deploy: |
|
||||||
resources: |
|
||||||
limits: |
|
||||||
cpus: "1.00" |
|
||||||
memory: 1G |
|
||||||
reservations: |
|
||||||
cpus: "0.50" |
|
||||||
memory: 512M |
|
||||||
depends_on: |
|
||||||
postgres: |
|
||||||
condition: service_healthy |
|
||||||
migrator: |
|
||||||
condition: service_completed_successfully |
|
||||||
ports: |
|
||||||
- "127.0.0.1:4000:4000" |
|
||||||
environment: |
|
||||||
DATABASE_URL: "ecto://${POSTGRES_RUNTIME_USER}:${POSTGRES_RUNTIME_PASSWORD}@${POSTGRES_HOST:-postgres}/${POSTGRES_DB}" |
|
||||||
SECRET_KEY_BASE: ${SECRET_KEY_BASE:?set SECRET_KEY_BASE in .env.prod} |
|
||||||
PHX_HOST: ${PHX_HOST:?set PHX_HOST in .env.prod (public hostname, no scheme)} |
|
||||||
networks: |
|
||||||
- internal |
|
||||||
|
|
||||||
networks: |
|
||||||
internal: |
|
||||||
|
|
||||||
volumes: |
|
||||||
pgdata: |
|
||||||
@ -1,50 +0,0 @@ |
|||||||
#!/usr/bin/env bash |
|
||||||
# Development server with auto-restart on config file changes. |
|
||||||
# |
|
||||||
# Normal code changes (controllers, templates, etc.) are still hot-reloaded |
|
||||||
# by Phoenix automatically. This script only handles the cases Phoenix can't: |
|
||||||
# config/config.exs, config/dev.exs, and config/runtime.exs. |
|
||||||
# |
|
||||||
# Usage: |
|
||||||
# chmod +x dev.sh |
|
||||||
# source .env && ./dev.sh |
|
||||||
|
|
||||||
set -euo pipefail |
|
||||||
|
|
||||||
CONFIG_FILES=( |
|
||||||
config/config.exs |
|
||||||
config/dev.exs |
|
||||||
config/runtime.exs |
|
||||||
) |
|
||||||
|
|
||||||
cleanup() { |
|
||||||
if [ -n "${SERVER_PID:-}" ] && kill -0 "$SERVER_PID" 2>/dev/null; then |
|
||||||
echo "" |
|
||||||
echo "[dev] Stopping server (pid $SERVER_PID)..." |
|
||||||
kill "$SERVER_PID" |
|
||||||
wait "$SERVER_PID" 2>/dev/null || true |
|
||||||
fi |
|
||||||
exit 0 |
|
||||||
} |
|
||||||
|
|
||||||
trap cleanup INT TERM |
|
||||||
|
|
||||||
echo "[dev] Watching config files for changes: ${CONFIG_FILES[*]}" |
|
||||||
echo "[dev] Normal code changes are still hot-reloaded automatically." |
|
||||||
echo "[dev] Press Ctrl+C to stop." |
|
||||||
echo "" |
|
||||||
|
|
||||||
while true; do |
|
||||||
echo "[dev] Starting server..." |
|
||||||
mix phx.server & |
|
||||||
SERVER_PID=$! |
|
||||||
|
|
||||||
# Block until any config file is modified |
|
||||||
inotifywait -q -e modify "${CONFIG_FILES[@]}" 2>/dev/null |
|
||||||
|
|
||||||
echo "" |
|
||||||
echo "[dev] Config changed — restarting server..." |
|
||||||
kill "$SERVER_PID" 2>/dev/null |
|
||||||
wait "$SERVER_PID" 2>/dev/null || true |
|
||||||
echo "" |
|
||||||
done |
|
||||||
@ -1,89 +0,0 @@ |
|||||||
#!/usr/bin/env bash |
|
||||||
# Production Docker Compose helper — run ./scripts/deploy_prod.sh --help |
|
||||||
|
|
||||||
set -euo pipefail |
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" |
|
||||||
cd "$ROOT" |
|
||||||
|
|
||||||
COMPOSE="${COMPOSE:-docker compose}" |
|
||||||
COMPOSE_FILE="${COMPOSE_FILE:-compose.prod.yml}" |
|
||||||
ENV_FILE="${ENV_FILE:-.env.prod}" |
|
||||||
|
|
||||||
APP_IMAGES=(setup relay migrator) |
|
||||||
|
|
||||||
usage() { |
|
||||||
cat <<'EOF' |
|
||||||
Production Docker Compose helper (compose.prod.yml). |
|
||||||
|
|
||||||
./scripts/deploy_prod.sh build build app images (setup, relay, migrator) |
|
||||||
./scripts/deploy_prod.sh push push those images (run docker login first) |
|
||||||
./scripts/deploy_prod.sh build-push build then push |
|
||||||
./scripts/deploy_prod.sh pull pull images |
|
||||||
./scripts/deploy_prod.sh up start stack (detached) |
|
||||||
./scripts/deploy_prod.sh deploy pull then up -d (typical on server) |
|
||||||
./scripts/deploy_prod.sh down stop stack |
|
||||||
./scripts/deploy_prod.sh ps docker compose ps |
|
||||||
./scripts/deploy_prod.sh logs [svc] follow logs (default: all services) |
|
||||||
|
|
||||||
Env: ENV_FILE (default .env.prod), COMPOSE_FILE, TAG, COMPOSE (default "docker compose") |
|
||||||
EOF |
|
||||||
exit "${1:-0}" |
|
||||||
} |
|
||||||
|
|
||||||
require_env_file() { |
|
||||||
if [[ ! -f "$ENV_FILE" ]]; then |
|
||||||
echo "error: missing env file: $ENV_FILE" >&2 |
|
||||||
echo " cp .env.prod.example .env.prod && edit, or set ENV_FILE=..." >&2 |
|
||||||
exit 1 |
|
||||||
fi |
|
||||||
} |
|
||||||
|
|
||||||
compose() { |
|
||||||
require_env_file |
|
||||||
$COMPOSE --env-file "$ENV_FILE" -f "$COMPOSE_FILE" "$@" |
|
||||||
} |
|
||||||
|
|
||||||
cmd="${1:-}" |
|
||||||
[[ -z "$cmd" ]] && usage 1 |
|
||||||
[[ "$cmd" == "-h" || "$cmd" == "--help" ]] && usage 0 |
|
||||||
shift || true |
|
||||||
|
|
||||||
case "$cmd" in |
|
||||||
help) |
|
||||||
usage 0 |
|
||||||
;; |
|
||||||
build) |
|
||||||
compose build "${APP_IMAGES[@]}" |
|
||||||
;; |
|
||||||
push) |
|
||||||
compose push "${APP_IMAGES[@]}" |
|
||||||
;; |
|
||||||
build-push) |
|
||||||
compose build "${APP_IMAGES[@]}" |
|
||||||
compose push "${APP_IMAGES[@]}" |
|
||||||
;; |
|
||||||
pull) |
|
||||||
compose pull |
|
||||||
;; |
|
||||||
up) |
|
||||||
compose up -d |
|
||||||
;; |
|
||||||
deploy) |
|
||||||
compose pull |
|
||||||
compose up -d |
|
||||||
;; |
|
||||||
down) |
|
||||||
compose down |
|
||||||
;; |
|
||||||
ps) |
|
||||||
compose ps "$@" |
|
||||||
;; |
|
||||||
logs) |
|
||||||
compose logs -f "$@" |
|
||||||
;; |
|
||||||
*) |
|
||||||
echo "error: unknown command: $cmd" >&2 |
|
||||||
usage 1 |
|
||||||
;; |
|
||||||
esac |
|
||||||
@ -0,0 +1,85 @@ |
|||||||
|
defmodule GcIndexRelayWeb.Plugs.CorsPlugTest do |
||||||
|
@moduledoc false |
||||||
|
use GcIndexRelayWeb.ConnCase, async: false |
||||||
|
|
||||||
|
@moduletag :unit |
||||||
|
|
||||||
|
setup do |
||||||
|
previous = Application.get_env(:gc_index_relay, :cors) |
||||||
|
on_exit(fn -> Application.put_env(:gc_index_relay, :cors, previous) end) |
||||||
|
{:ok, previous_cors: previous} |
||||||
|
end |
||||||
|
|
||||||
|
describe "allow_origins allowlist" do |
||||||
|
setup %{previous_cors: previous} do |
||||||
|
base = previous || [] |
||||||
|
|
||||||
|
Application.put_env( |
||||||
|
:gc_index_relay, |
||||||
|
:cors, |
||||||
|
Keyword.merge(base, |
||||||
|
enabled: true, |
||||||
|
allow_origins: ["https://client.example.com"] |
||||||
|
) |
||||||
|
) |
||||||
|
|
||||||
|
:ok |
||||||
|
end |
||||||
|
|
||||||
|
test "reflects Origin when it matches the allowlist", %{conn: conn} do |
||||||
|
conn = |
||||||
|
conn |
||||||
|
|> put_req_header("accept", "application/json") |
||||||
|
|> put_req_header("origin", "https://client.example.com") |
||||||
|
|> get(~p"/api") |
||||||
|
|
||||||
|
assert get_resp_header(conn, "access-control-allow-origin") == [ |
||||||
|
"https://client.example.com" |
||||||
|
] |
||||||
|
end |
||||||
|
|
||||||
|
test "omits Access-Control-Allow-Origin when Origin does not match", %{conn: conn} do |
||||||
|
conn = |
||||||
|
conn |
||||||
|
|> put_req_header("accept", "application/json") |
||||||
|
|> put_req_header("origin", "https://evil.example.com") |
||||||
|
|> get(~p"/api") |
||||||
|
|
||||||
|
assert get_resp_header(conn, "access-control-allow-origin") == [] |
||||||
|
end |
||||||
|
|
||||||
|
test "preflight without matching Origin has no CORS allow headers", %{conn: conn} do |
||||||
|
conn = |
||||||
|
conn |
||||||
|
|> put_req_header("origin", "https://evil.example.com") |
||||||
|
|> options("/api/events") |
||||||
|
|
||||||
|
assert conn.status == 200 |
||||||
|
assert conn.resp_body == "" |
||||||
|
assert get_resp_header(conn, "access-control-allow-origin") == [] |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
describe "CORS disabled" do |
||||||
|
setup %{previous_cors: previous} do |
||||||
|
base = previous || [] |
||||||
|
Application.put_env(:gc_index_relay, :cors, Keyword.merge(base, enabled: false)) |
||||||
|
:ok |
||||||
|
end |
||||||
|
|
||||||
|
test "does not set CORS headers on GET", %{conn: conn} do |
||||||
|
conn = |
||||||
|
conn |
||||||
|
|> put_req_header("accept", "application/json") |
||||||
|
|> get(~p"/api") |
||||||
|
|
||||||
|
assert get_resp_header(conn, "access-control-allow-origin") == [] |
||||||
|
end |
||||||
|
|
||||||
|
test "does not handle OPTIONS preflight (no CORS headers)", %{conn: conn} do |
||||||
|
conn = options(conn, "/api/events") |
||||||
|
|
||||||
|
assert get_resp_header(conn, "access-control-allow-origin") == [] |
||||||
|
end |
||||||
|
end |
||||||
|
end |
||||||
Loading…
Reference in new issue