14 changed files with 207 additions and 292 deletions
@ -1,15 +0,0 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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