From d9415e6a96108df3f9ff361c908951e00009b059 Mon Sep 17 00:00:00 2001 From: Mark Mennell Date: Sat, 9 May 2026 15:57:13 +0800 Subject: [PATCH 1/7] run local scripts --- compose/addresses.csv | 5 + scripts/del-local-dev.sh | 105 +++++++++++++ scripts/run-local-dev.sh | 306 ++++++++++++++++++++++++++++++++++++++ scripts/stop-local-dev.sh | 94 ++++++++++++ 4 files changed, 510 insertions(+) create mode 100644 compose/addresses.csv create mode 100644 scripts/del-local-dev.sh create mode 100644 scripts/run-local-dev.sh create mode 100644 scripts/stop-local-dev.sh diff --git a/compose/addresses.csv b/compose/addresses.csv new file mode 100644 index 0000000..462208b --- /dev/null +++ b/compose/addresses.csv @@ -0,0 +1,5 @@ +address,display_name,accepting_new,limit_recv_size_total,limit_recv_size_per_msg,limit_recv_size_per_1d,limit_recv_count_per_1d,limit_send_size_total,limit_send_size_per_msg,limit_send_size_per_1d,limit_send_count_per_1d +@alice@__FMSG_DOMAIN__,Alice,true,102400000,10240,102400,1000,102400000,10240,102400,1000 +@bob@__FMSG_DOMAIN__,Bob,true,102400000,10240,102400,1000,102400000,10240,102400,1000 +@carol@__FMSG_DOMAIN__,Carol,true,102400000,10240,102400,1000,102400000,10240,102400,1000 +@dave@__FMSG_DOMAIN__,Dave,true,102400000,10240,102400,1000,102400000,10240,102400,1000 diff --git a/scripts/del-local-dev.sh b/scripts/del-local-dev.sh new file mode 100644 index 0000000..a4dc2dc --- /dev/null +++ b/scripts/del-local-dev.sh @@ -0,0 +1,105 @@ +#!/usr/bin/env bash +# ============================================================= +# Delete the local development fmsg compose stack, including its +# compose-managed containers, networks, and named volumes. +# +# This also removes generated local-development certs, rendered CSVs, +# and the generated compose override under .bin/local-dev. +# ============================================================= +set -euo pipefail + +usage() { + cat <<'EOF' +Usage: + ./scripts/del-local-dev.sh [domain] + +Examples: + ./scripts/del-local-dev.sh hairpin.local + +Environment overrides: + COMPOSE_PROJECT_NAME default: fmsg_ +EOF +} + +require_command() { + local command_name="$1" + + if ! command -v "$command_name" >/dev/null 2>&1; then + echo "Missing required command: $command_name" >&2 + exit 1 + fi +} + +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd -- "$script_dir/.." && pwd)" +compose_dir="$repo_root/compose" +local_dev_dir="$repo_root/.bin/local-dev" +local_override="$local_dev_dir/docker-compose.local-dev.yml" + +if [[ $# -gt 1 ]]; then + usage + exit 1 +fi + +domain="${1:-}" + +if [[ "$domain" == "-h" || "$domain" == "--help" ]]; then + usage + exit 0 +fi + +if [[ -z "$domain" ]]; then + read -r -p "Local fmsg domain [hairpin.local]: " domain + domain="${domain:-hairpin.local}" +fi + +if [[ "$domain" == fmsg.* || "$domain" == fmsgapi.* ]]; then + echo "Use the base domain only, for example: hairpin.local" >&2 + exit 1 +fi + +sanitized_domain="$(printf '%s' "$domain" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/_/g')" + +export FMSG_DOMAIN="$domain" +export COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-fmsg_${sanitized_domain}}" +export CERTBOT_EMAIL="${CERTBOT_EMAIL:-local-dev@${domain}}" +export FMSG_API_JWT_SECRET="${FMSG_API_JWT_SECRET:-local-dev-secret}" +export FMSG_PORT="${FMSG_PORT:-4930}" +export FMSG_WEBAPI_HOST_PORT="${FMSG_WEBAPI_HOST_PORT:-8181}" +export FMSG_API_PORT="${FMSG_API_PORT:-8000}" +export FMSGID_PORT="${FMSGID_PORT:-8080}" +export GIN_MODE="${GIN_MODE:-debug}" +export PGUSER="${PGUSER:-postgres}" +export PGPASSWORD="${PGPASSWORD:-test}" +export FMSGD_WRITER_PGPASSWORD="${FMSGD_WRITER_PGPASSWORD:-test}" +export FMSGD_READER_PGPASSWORD="${FMSGD_READER_PGPASSWORD:-test}" +export FMSGID_WRITER_PGPASSWORD="${FMSGID_WRITER_PGPASSWORD:-test}" +export FMSGID_READER_PGPASSWORD="${FMSGID_READER_PGPASSWORD:-test}" +export FMSG_SKIP_DOMAIN_IP_CHECK="${FMSG_SKIP_DOMAIN_IP_CHECK:-true}" +export FMSG_SKIP_AUTHORISED_IPS="${FMSG_SKIP_AUTHORISED_IPS:-true}" +export FMSG_TLS_INSECURE_SKIP_VERIFY="${FMSG_TLS_INSECURE_SKIP_VERIFY:-true}" + +compose_files=(-f docker-compose.yml) +if [[ -f "$local_override" ]]; then + compose_files+=(-f "$local_override") +fi + +require_command docker +docker compose version >/dev/null + +cd "$compose_dir" + +echo "==> Deleting local fmsg stack" +echo " domain: $FMSG_DOMAIN" +echo " compose project: $COMPOSE_PROJECT_NAME" + +docker compose "${compose_files[@]}" down -v --remove-orphans + +rm -rf "$local_dev_dir" + +if docker network inspect fmsg-local >/dev/null 2>&1; then + docker network rm fmsg-local >/dev/null 2>&1 || \ + echo "==> Shared Docker network fmsg-local is still in use; leaving it in place." +fi + +echo "==> Local fmsg stack deleted. Compose volumes and generated local files were removed." diff --git a/scripts/run-local-dev.sh b/scripts/run-local-dev.sh new file mode 100644 index 0000000..70b087e --- /dev/null +++ b/scripts/run-local-dev.sh @@ -0,0 +1,306 @@ +#!/usr/bin/env bash +# ============================================================= +# Run the fmsg compose stack for local development. +# +# DNS / hosts setup: +# Pick a base domain such as hairpin.local. FMSG_DOMAIN must be +# the base domain, not fmsg.hairpin.local. +# +# Add the base name and service names to your OS hosts file so +# tools on the host resolve them to localhost: +# +# 127.0.0.1 hairpin.local fmsg.hairpin.local fmsgapi.hairpin.local +# +# Windows hosts file: +# C:\Windows\System32\drivers\etc\hosts +# +# Linux/macOS hosts file: +# /etc/hosts +# +# TLS notes: +# fmsgd requires TCP+TLS, so this script generates a self-signed +# certificate for fmsg. and mounts it into the fmsgd container. +# Local outbound certificate verification is skipped with +# FMSG_TLS_INSECURE_SKIP_VERIFY=true. +# +# fmsg-webapi supports plain HTTP for development when FMSG_TLS_CERT +# and FMSG_TLS_KEY are omitted, so this script disables webapi TLS and +# exposes it on http://localhost:${FMSG_WEBAPI_HOST_PORT:-8181}. +# +# JWT notes: +# fmsg-webapi validates JWTs using a separate local IdP. The issuer is +# the host-facing URL (default http://localhost:8080). The JWKS URL must +# be reachable from inside the fmsg-webapi container, so the default uses +# host.docker.internal rather than localhost. +# ============================================================= +set -euo pipefail + +usage() { + cat <<'EOF' +Usage: + ./scripts/run-local-dev.sh [domain] [addresses.csv] + +Examples: + ./scripts/run-local-dev.sh hairpin.local + ./scripts/run-local-dev.sh hairpin.local ./addresses.csv + +Environment overrides: + COMPOSE_PROJECT_NAME default: fmsg_ + FMSG_PORT default: 4930 + FMSG_WEBAPI_HOST_PORT default: 8181 + POSTGRES_HOST_PORT default: 54321 + FMSG_JWT_JWKS_URL default: http://host.docker.internal:8080/.well-known/jwks.json + IDP_JWT_ISSUER default: http://localhost:8080 + FMSG_JWT_ISSUER default: value of IDP_JWT_ISSUER + FMSG_CORS_ORIGINS default: http://localhost:8081 + FMSG_ADDRESSES_CSV optional path/template copied to fmsgid addresses.csv + FMSGD_WRITER_PGPASSWORD default: test + FMSGD_READER_PGPASSWORD default: test + FMSGID_WRITER_PGPASSWORD default: test + FMSGID_READER_PGPASSWORD default: test + PGPASSWORD default: test +EOF +} + +require_command() { + local command_name="$1" + + if ! command -v "$command_name" >/dev/null 2>&1; then + echo "Missing required command: $command_name" >&2 + exit 1 + fi +} + +print_client_env() { + cat <&2 + echo "Do not use fmsg.$domain or fmsgapi.$domain as FMSG_DOMAIN." >&2 + exit 1 +fi + +if [[ -n "$addresses_csv" && "$addresses_csv" != /* && ! "$addresses_csv" =~ ^[A-Za-z]: ]]; then + addresses_csv="$initial_dir/$addresses_csv" +fi + +sanitized_domain="$(printf '%s' "$domain" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/_/g')" + +export FMSG_DOMAIN="$domain" +export COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-fmsg_${sanitized_domain}}" +export CERTBOT_EMAIL="${CERTBOT_EMAIL:-local-dev@${domain}}" +export IDP_JWT_ISSUER="${IDP_JWT_ISSUER:-http://localhost:8080}" +export FMSG_JWT_ISSUER="${FMSG_JWT_ISSUER:-$IDP_JWT_ISSUER}" +export FMSG_JWT_JWKS_URL="${FMSG_JWT_JWKS_URL:-http://host.docker.internal:8080/.well-known/jwks.json}" +export FMSG_CORS_ORIGINS="${FMSG_CORS_ORIGINS:-http://localhost:8081}" +export FMSG_API_JWT_SECRET="${FMSG_API_JWT_SECRET:-}" +export FMSG_PORT="${FMSG_PORT:-4930}" +export FMSG_WEBAPI_HOST_PORT="${FMSG_WEBAPI_HOST_PORT:-8181}" +export POSTGRES_HOST_PORT="${POSTGRES_HOST_PORT:-54321}" +export FMSG_API_PORT="${FMSG_API_PORT:-8000}" +export FMSGID_PORT="${FMSGID_PORT:-8080}" +export GIN_MODE="${GIN_MODE:-debug}" +export PGUSER="${PGUSER:-postgres}" +export PGPASSWORD="${PGPASSWORD:-test}" +export FMSGD_WRITER_PGPASSWORD="${FMSGD_WRITER_PGPASSWORD:-test}" +export FMSGD_READER_PGPASSWORD="${FMSGD_READER_PGPASSWORD:-test}" +export FMSGID_WRITER_PGPASSWORD="${FMSGID_WRITER_PGPASSWORD:-test}" +export FMSGID_READER_PGPASSWORD="${FMSGID_READER_PGPASSWORD:-test}" +export FMSG_SKIP_DOMAIN_IP_CHECK="${FMSG_SKIP_DOMAIN_IP_CHECK:-true}" +export FMSG_SKIP_AUTHORISED_IPS="${FMSG_SKIP_AUTHORISED_IPS:-true}" +export FMSG_TLS_INSECURE_SKIP_VERIFY="${FMSG_TLS_INSECURE_SKIP_VERIFY:-true}" + +require_command docker +docker compose version >/dev/null + +cd "$compose_dir" + +compose_files=(-f docker-compose.yml) +if [[ -f "$local_override" ]]; then + compose_files+=(-f "$local_override") + + if grep -Fq -- "FMSG_JWT_JWKS_URL: \"${FMSG_JWT_JWKS_URL}\"" "$local_override" && \ + grep -Fq -- "FMSG_JWT_ISSUER: \"${FMSG_JWT_ISSUER}\"" "$local_override" && \ + grep -Fq -- "FMSG_CORS_ORIGINS: \"${FMSG_CORS_ORIGINS}\"" "$local_override" && \ + grep -Fq -- "- \"${POSTGRES_HOST_PORT}:5432\"" "$local_override" && \ + grep -Fq -- "- \"${FMSG_WEBAPI_HOST_PORT}:${FMSG_API_PORT}\"" "$local_override"; then + existing_container_ids="$(docker compose "${compose_files[@]}" ps -a -q 2>/dev/null || true)" + running_container_ids="$(docker compose "${compose_files[@]}" ps -q 2>/dev/null || true)" + + if [[ -n "$existing_container_ids" && -z "$running_container_ids" ]]; then + echo "==> Starting existing local fmsg stack" + echo " domain: $FMSG_DOMAIN" + echo " compose project: $COMPOSE_PROJECT_NAME" + echo " fmsgd: fmsg.$FMSG_DOMAIN:$FMSG_PORT" + echo " fmsg-webapi: http://localhost:$FMSG_WEBAPI_HOST_PORT" + echo " postgres: localhost:$POSTGRES_HOST_PORT" + + docker compose "${compose_files[@]}" start + print_client_env + exit 0 + fi + fi +fi + +require_command openssl + +if ! docker network inspect fmsg-local >/dev/null 2>&1; then + echo "==> Creating shared Docker network: fmsg-local" + docker network create fmsg-local >/dev/null +fi + +mkdir -p "$tls_dir" + +cert_file="$tls_dir/fmsg.${FMSG_DOMAIN}.crt" +key_file="$tls_dir/fmsg.${FMSG_DOMAIN}.key" + +if [[ ! -s "$cert_file" || ! -s "$key_file" ]]; then + echo "==> Generating self-signed TLS certificate for fmsg.${FMSG_DOMAIN}" + openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 \ + -keyout "$key_file" \ + -out "$cert_file" \ + -days 30 -nodes \ + -subj "//CN=fmsg.${FMSG_DOMAIN}" \ + -addext "subjectAltName=DNS:fmsg.${FMSG_DOMAIN},DNS:${FMSG_DOMAIN},DNS:localhost,IP:127.0.0.1" +else + echo "==> Using existing local TLS certificate: $cert_file" +fi + +chmod 0644 "$cert_file" "$key_file" 2>/dev/null || true + +cat > "$local_override" < Starting local fmsg stack" +echo " domain: $FMSG_DOMAIN" +echo " compose project: $COMPOSE_PROJECT_NAME" +echo " fmsgd: fmsg.$FMSG_DOMAIN:$FMSG_PORT" +echo " fmsg-webapi: http://localhost:$FMSG_WEBAPI_HOST_PORT" +echo " postgres: localhost:$POSTGRES_HOST_PORT" + +docker compose -f docker-compose.yml -f "$local_override" up -d --build --wait + +if [[ -n "$addresses_csv" ]]; then + if [[ ! -f "$addresses_csv" ]]; then + echo "addresses.csv not found: $addresses_csv" >&2 + exit 1 + fi + + rendered_addresses_csv="$local_dev_dir/addresses.csv" + sed_domain="$(printf '%s' "$FMSG_DOMAIN" | sed 's/[&|\\]/\\&/g')" + sed "s|__FMSG_DOMAIN__|$sed_domain|g" "$addresses_csv" > "$rendered_addresses_csv" + + echo "==> Copying addresses CSV to fmsgid: $addresses_csv" + docker compose -f docker-compose.yml -f "$local_override" cp \ + "$rendered_addresses_csv" fmsgid:/opt/fmsgid/data/addresses.csv +else + echo "==> No addresses.csv copied. Set FMSG_ADDRESSES_CSV or pass a second argument to seed users." +fi + +print_client_env diff --git a/scripts/stop-local-dev.sh b/scripts/stop-local-dev.sh new file mode 100644 index 0000000..fb5e75c --- /dev/null +++ b/scripts/stop-local-dev.sh @@ -0,0 +1,94 @@ +#!/usr/bin/env bash +# ============================================================= +# Stop the local development fmsg compose stack without deleting +# containers, volumes, generated certs, or rendered CSV files. +# ============================================================= +set -euo pipefail + +usage() { + cat <<'EOF' +Usage: + ./scripts/stop-local-dev.sh [domain] + +Examples: + ./scripts/stop-local-dev.sh hairpin.local + +Environment overrides: + COMPOSE_PROJECT_NAME default: fmsg_ +EOF +} + +require_command() { + local command_name="$1" + + if ! command -v "$command_name" >/dev/null 2>&1; then + echo "Missing required command: $command_name" >&2 + exit 1 + fi +} + +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd -- "$script_dir/.." && pwd)" +compose_dir="$repo_root/compose" +local_override="$repo_root/.bin/local-dev/docker-compose.local-dev.yml" + +if [[ $# -gt 1 ]]; then + usage + exit 1 +fi + +domain="${1:-}" + +if [[ "$domain" == "-h" || "$domain" == "--help" ]]; then + usage + exit 0 +fi + +if [[ -z "$domain" ]]; then + read -r -p "Local fmsg domain [hairpin.local]: " domain + domain="${domain:-hairpin.local}" +fi + +if [[ "$domain" == fmsg.* || "$domain" == fmsgapi.* ]]; then + echo "Use the base domain only, for example: hairpin.local" >&2 + exit 1 +fi + +sanitized_domain="$(printf '%s' "$domain" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/_/g')" + +export FMSG_DOMAIN="$domain" +export COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-fmsg_${sanitized_domain}}" +export CERTBOT_EMAIL="${CERTBOT_EMAIL:-local-dev@${domain}}" +export FMSG_API_JWT_SECRET="${FMSG_API_JWT_SECRET:-local-dev-secret}" +export FMSG_PORT="${FMSG_PORT:-4930}" +export FMSG_WEBAPI_HOST_PORT="${FMSG_WEBAPI_HOST_PORT:-8181}" +export FMSG_API_PORT="${FMSG_API_PORT:-8000}" +export FMSGID_PORT="${FMSGID_PORT:-8080}" +export GIN_MODE="${GIN_MODE:-debug}" +export PGUSER="${PGUSER:-postgres}" +export PGPASSWORD="${PGPASSWORD:-test}" +export FMSGD_WRITER_PGPASSWORD="${FMSGD_WRITER_PGPASSWORD:-test}" +export FMSGD_READER_PGPASSWORD="${FMSGD_READER_PGPASSWORD:-test}" +export FMSGID_WRITER_PGPASSWORD="${FMSGID_WRITER_PGPASSWORD:-test}" +export FMSGID_READER_PGPASSWORD="${FMSGID_READER_PGPASSWORD:-test}" +export FMSG_SKIP_DOMAIN_IP_CHECK="${FMSG_SKIP_DOMAIN_IP_CHECK:-true}" +export FMSG_SKIP_AUTHORISED_IPS="${FMSG_SKIP_AUTHORISED_IPS:-true}" +export FMSG_TLS_INSECURE_SKIP_VERIFY="${FMSG_TLS_INSECURE_SKIP_VERIFY:-true}" + +compose_files=(-f docker-compose.yml) +if [[ -f "$local_override" ]]; then + compose_files+=(-f "$local_override") +fi + +require_command docker +docker compose version >/dev/null + +cd "$compose_dir" + +echo "==> Stopping local fmsg stack" +echo " domain: $FMSG_DOMAIN" +echo " compose project: $COMPOSE_PROJECT_NAME" + +docker compose "${compose_files[@]}" stop + +echo "==> Local fmsg stack stopped. Volumes and generated local files were preserved." From 1b4292834dbfbb7d5c776f7c80c2f3ce422d3d02 Mon Sep 17 00:00:00 2001 From: Mark Mennell Date: Wed, 1 Jul 2026 18:12:27 +0800 Subject: [PATCH 2/7] wip --- TMP_LOCAL_DEV.md | 247 ++++++++++++++++++++++ compose/.env.local | 32 +++ compose/docker-compose.local.yml | 45 ++++ compose/docker-compose.yml | 258 +++++++++++------------ docker/postgres/init/999-permissions.sql | 98 ++++----- 5 files changed, 502 insertions(+), 178 deletions(-) create mode 100644 TMP_LOCAL_DEV.md create mode 100644 compose/.env.local create mode 100644 compose/docker-compose.local.yml diff --git a/TMP_LOCAL_DEV.md b/TMP_LOCAL_DEV.md new file mode 100644 index 0000000..f8fa580 --- /dev/null +++ b/TMP_LOCAL_DEV.md @@ -0,0 +1,247 @@ +# Local Development (no domain, no IdP, Podman) + +This describes running the fmsg-docker stack entirely on your own machine: + +- No real domain name (uses `fmsg.local.test` / `fmsgapi.local.test` resolved via `/etc/hosts`) +- No Let's Encrypt / certbot (self-signed TLS certs instead) +- No external IdP / JWKS login (the `FMSG_JWT_*` vars are optional — leave them unset and + auth flows entirely through fmsg-webapi's own API key operator command, same as QUICKSTART.md) +- Podman instead of Docker + +This is basically the same trick `test/docker-compose.test.yml` and `test/run-tests.sh` already +use for integration tests (self-signed certs + `FMSG_SKIP_DOMAIN_IP_CHECK` + certbot disabled), +just simplified to a single instance for interactive local dev. + +## 1. Podman setup + +Docker Compose files work unmodified with Podman via the `podman-compose` project, or with +Podman's own `podman compose` command (Podman 4.7+ ships a Docker Compose-compatible frontend +that shells out to `docker-compose`/`podman-compose` under the hood). + +Check what you have: + +```sh +podman compose version # built-in, if available +# or +podman-compose version # separate package, e.g. `dnf install podman-compose` +``` + +Everywhere below that says `docker compose`, substitute `podman compose` or `podman-compose`. +Rootless Podman can't bind ports below 1024 without extra privileges, so this guide maps +`fmsg-webapi` and the ACME port to unprivileged host ports (see the override file below). + +Optional convenience alias so you don't have to remember to swap commands: + +```sh +alias docker=podman +``` + +## 2. Add local hostnames + +fmsgd and fmsg-webapi expect to serve `fmsg.` and `fmsgapi.` respectively. +Pick a fake domain and point it at your machine via `/etc/hosts`: + +```sh +echo "127.0.0.1 fmsg.local.test fmsgapi.local.test" | sudo tee -a /etc/hosts +``` + +## 3. Generate self-signed TLS certs + +Skip certbot/Let's Encrypt entirely (it requires a real domain reachable on port 80) and +generate certs the same way `test/run-tests.sh` does: + +```sh +mkdir -p test/.tls +for name in fmsg.local.test fmsgapi.local.test; do + openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 \ + -keyout "test/.tls/${name}.key" \ + -out "test/.tls/${name}.crt" \ + -days 365 -nodes \ + -subj "//CN=${name}" \ + -addext "subjectAltName=DNS:${name}" +done +chmod 644 test/.tls/*.key +``` + +## 4. Local-dev compose override + +Create `compose/docker-compose.local.yml` next to `docker-compose.yml`: + +Note the certs live on the host at `test/.tls/` (step 3), but `FMSG_TLS_CERT`/`FMSG_TLS_KEY` +are paths *inside the container*. The `volumes:` line below mounts the host's `../test/.tls` +(relative to `compose/`, i.e. `test/.tls` at the repo root) to `/opt/fmsg/tls` inside each +container, so `/opt/fmsg/tls/fmsg.${FMSG_DOMAIN}.crt` resolves to the +`test/.tls/fmsg.local.test.crt` file generated in step 3. + +```yaml +services: + + certbot: + entrypoint: ["true"] + restart: "no" + ports: !override [] + + postgres: + ports: + - "5432:5432" + + fmsgd: + environment: + FMSG_TLS_CERT: /opt/fmsg/tls/fmsg.${FMSG_DOMAIN}.crt + FMSG_TLS_KEY: /opt/fmsg/tls/fmsg.${FMSG_DOMAIN}.key + volumes: + - ../test/.tls:/opt/fmsg/tls:ro + depends_on: !override + postgres: + condition: service_healthy + fmsgid: + condition: service_started + + fmsg-webapi: + environment: + FMSG_TLS_CERT: /opt/fmsg/tls/fmsgapi.${FMSG_DOMAIN}.crt + FMSG_TLS_KEY: /opt/fmsg/tls/fmsgapi.${FMSG_DOMAIN}.key + volumes: + - ../test/.tls:/opt/fmsg/tls:ro + depends_on: !override + fmsgd: + condition: service_started + fmsgid: + condition: service_started + ports: !override + - "8443:${FMSG_API_PORT:-8000}" +``` + +This disables certbot, points both services at the self-signed certs from step 3, remaps +`fmsg-webapi` to host port `8443` so rootless Podman doesn't need to bind `443`, and exposes +`postgres` on host port `5432` so you can connect directly with `psql` or a GUI client while +developing (e.g. `psql -h localhost -U postgres`, password from `PGPASSWORD` in step 6). + +## 5. Configure env + +Compose auto-loads a file literally named `.env` in the working directory, so reusing that +name risks silently overwriting (or being confused with) a real `compose/.env` you already +have configured for a non-local deployment. Use a separate `compose/.env.local` instead, and +pass it explicitly with `--env-file` — this is the standard way to keep multiple compose env +profiles side by side without clobbering the default one: + +```sh +cp .env.example compose/.env.local +``` + +Edit `compose/.env.local`: + +```env +FMSG_DOMAIN=local.test +CERTBOT_EMAIL=dev@local.test # unused, certbot is disabled, but the var is required +FMSG_API_TOKEN_ED25519_PRIVATE_KEY= +FMSGD_WRITER_PGPASSWORD=devpassword +FMSGID_WRITER_PGPASSWORD=devpassword +FMSG_SKIP_DOMAIN_IP_CHECK=true +FMSG_SKIP_AUTHORISED_IPS=true +``` + +Leave the `FMSG_JWT_*` vars unset — that's what opts out of JWKS/IdP login. + +`FMSG_SKIP_DOMAIN_IP_CHECK=true` is required because fmsgd normally verifies your domain's DNS +resolves to the host's public IP, which a fake local domain never will. `FMSG_SKIP_AUTHORISED_IPS=true` +avoids needing real allow-listed peer IPs for host-to-host TCP. + +Because `compose/.env.local` isn't auto-loaded, every `podman compose` invocation below passes +`--env-file .env.local` explicitly (paths are relative to `compose/`, where these commands are +run from). If you forget it, compose falls back to `compose/.env` (or nothing), and the local +overrides silently won't apply. + +## 6. Start the stack + +```sh +cd compose +PGPASSWORD=devpgpass \ +FMSGD_READER_PGPASSWORD=devpassword \ +FMSGID_READER_PGPASSWORD=devpassword \ +podman compose --env-file .env.local -f docker-compose.yml -f docker-compose.local.yml up -d --build +``` + +Check everything came up: + +```sh +podman compose --env-file .env.local -f docker-compose.yml -f docker-compose.local.yml ps +``` + +fmsg-webapi is now reachable at `https://fmsgapi.local.test:8443` (self-signed, so clients need +`-k`/insecure-skip-verify), and fmsgd is listening on TCP `4930` for host-to-host traffic. + +## Stopping the stack + +Stops containers but keeps them (and their data volumes) around so `up -d` again is fast and +your postgres data / issued certs persist: + +```sh +cd compose +podman compose --env-file .env.local -f docker-compose.yml -f docker-compose.local.yml stop +``` + +Restart later with: + +```sh +podman compose --env-file .env.local -f docker-compose.yml -f docker-compose.local.yml start +``` + +## Stopping and removing the stack + +Removes the containers (and network), but by default leaves named volumes (`postgres_data`, +`fmsg_data`, `fmsgid_data`, `letsencrypt`) intact: + +```sh +cd compose +podman compose --env-file .env.local -f docker-compose.yml -f docker-compose.local.yml down +``` + +To also wipe all persisted data (postgres DB, fmsg/fmsgid data, certs) and start completely +fresh next time, add `-v`: + +```sh +podman compose --env-file .env.local -f docker-compose.yml -f docker-compose.local.yml down -v +``` + +Then remove the self-signed TLS certs generated in step 3 too, since they're outside the +compose volumes: + +```sh +rm -rf ../test/.tls +``` + +## 7. Add a user and API key + +Same as QUICKSTART.md's "Next Steps", just against the local containers: + +```sh +printf 'address,display_name,accepting_new,limit_recv_size_total,limit_recv_size_per_msg,limit_recv_size_per_1d,limit_recv_count_per_1d,limit_send_size_total,limit_send_size_per_msg,limit_send_size_per_1d,limit_send_count_per_1d\n@alice@local.test,Alice,true,102400000,10240,102400,1000,102400000,10240,102400,1000\n' > addresses.csv + +podman compose --env-file .env.local -f docker-compose.yml -f docker-compose.local.yml cp addresses.csv fmsgid:/opt/fmsgid/data/addresses.csv + +podman compose --env-file .env.local -f docker-compose.yml -f docker-compose.local.yml exec fmsg-webapi \ + /opt/fmsg-webapi/fmsg-webapi api-key create-delegation \ + -owner @alice@local.test \ + -agent cli \ + -addr @alice@local.test \ + -cidr 127.0.0.1/32 \ + -expires 2099-01-01T00:00:00Z +``` + +Then use [fmsg-cli](https://github.com/markmnl/fmsg-cli) against it: + +```sh +FMSG_API_URL=https://fmsgapi.local.test:8443 \ +FMSG_API_KEY=fmsgk__ \ +FMSG_TLS_INSECURE_SKIP_VERIFY=true \ +fmsg list +``` + +## TODO before folding into README.md + +- Confirm exact `podman compose` / `podman-compose` invocation on this machine works as-is + (rootless networking between containers, volume permissions). +- Confirm `fmsg-cli` actually supports `FMSG_TLS_INSECURE_SKIP_VERIFY`, or find the right flag. +- Decide whether this override file should be checked into the repo (e.g. as + `compose/docker-compose.dev.yml`) rather than living only in this doc. diff --git a/compose/.env.local b/compose/.env.local new file mode 100644 index 0000000..9dbcad5 --- /dev/null +++ b/compose/.env.local @@ -0,0 +1,32 @@ +# ── Required ────────────────────────────────────────────── + +# fmsg domain name this host serves +# - fmsgd will uses TCP 4930 on fmsg. +# - fmsg-webapi uses HTTPS 443 on fmsgapi. +FMSG_DOMAIN=local.test + +# Email address for Let's Encrypt certificate registration +CERTBOT_EMAIL=dev@local.test + +# Base64-encoded Ed25519 seed or private key used by fmsg-webapi to mint +# short-lived first-party JWTs from API keys. Generate with: +# openssl rand -base64 32 +FMSG_API_TOKEN_ED25519_PRIVATE_KEY=pp/6hCThzDKIUw50fg6ybAuahYcKwG1cyplr4Lb3a+o= + +# Per-service database passwords (used by application services) +FMSGD_WRITER_PGPASSWORD=devpassword +FMSGID_WRITER_PGPASSWORD=devpassword + +# ── Optional (defaults shown) ──────────────────────────── + +# FMSG_PORT=4930 +# FMSGID_PORT=8080 +# GIN_MODE=release +FMSG_SKIP_DOMAIN_IP_CHECK=true +FMSG_SKIP_AUTHORISED_IPS=true + +# External user JWT login (optional; set all four to enable JWKS auth) +# FMSG_JWT_JWKS_URL=https://idp.example.com/.well-known/jwks.json +# FMSG_JWT_ISSUER=https://idp.example.com/ +# FMSG_JWT_AUDIENCE=fmsg-web-client +# FMSG_JWT_ADDRESS_CLAIM=fmsg_address diff --git a/compose/docker-compose.local.yml b/compose/docker-compose.local.yml new file mode 100644 index 0000000..7521f3b --- /dev/null +++ b/compose/docker-compose.local.yml @@ -0,0 +1,45 @@ +services: + certbot: + entrypoint: ["true"] + restart: "no" + ports: !override [] + + postgres: + security_opt: + - label=disable + ports: + - "5432:5432" + + fmsgd: + environment: + FMSG_TLS_CERT: /opt/fmsg/tls/fmsg.${FMSG_DOMAIN}.crt + FMSG_TLS_KEY: /opt/fmsg/tls/fmsg.${FMSG_DOMAIN}.key + security_opt: + - label=disable + volumes: + - ../test/.tls:/opt/fmsg/tls:ro + depends_on: + postgres: + condition: service_started + fmsgid: + condition: service_started + extra_hosts: + - "fmsgapi.local.test:127.0.0.1" + + fmsg-webapi: + environment: + FMSG_TLS_CERT: /opt/fmsg/tls/fmsgapi.${FMSG_DOMAIN}.crt + FMSG_TLS_KEY: /opt/fmsg/tls/fmsgapi.${FMSG_DOMAIN}.key + security_opt: + - label=disable + volumes: + - ../test/.tls:/opt/fmsg/tls:ro + depends_on: + fmsgd: + condition: service_started + fmsgid: + condition: service_started + ports: !override + - "8443:8000" + extra_hosts: + - "fmsg.local.test:127.0.0.1" diff --git a/compose/docker-compose.yml b/compose/docker-compose.yml index bff8971..a0d29e9 100644 --- a/compose/docker-compose.yml +++ b/compose/docker-compose.yml @@ -1,106 +1,106 @@ -services: - - certbot: - image: certbot/certbot - restart: "no" - entrypoint: ["/bin/sh", "/entrypoint.sh"] - environment: - FMSG_DOMAIN: ${FMSG_DOMAIN} - CERTBOT_EMAIL: ${CERTBOT_EMAIL} - volumes: - - letsencrypt:/etc/letsencrypt - - ../docker/certbot/entrypoint.sh:/entrypoint.sh:ro - ports: - - "80:80" - - postgres: - image: postgres:18-alpine - restart: unless-stopped - environment: - POSTGRES_USER: ${PGUSER:-postgres} - POSTGRES_PASSWORD: ${PGPASSWORD} - POSTGRES_DB: postgres - FMSGD_WRITER_PGPASSWORD: ${FMSGD_WRITER_PGPASSWORD} - FMSGD_READER_PGPASSWORD: ${FMSGD_READER_PGPASSWORD} - FMSGID_WRITER_PGPASSWORD: ${FMSGID_WRITER_PGPASSWORD} - FMSGID_READER_PGPASSWORD: ${FMSGID_READER_PGPASSWORD} - volumes: - - postgres_data:/var/lib/postgresql - - ../docker/postgres/init:/docker-entrypoint-initdb.d:ro - healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${PGUSER:-postgres}"] - interval: 10s - timeout: 5s - retries: 5 - - fmsgid: - build: - context: ../docker/fmsgid - dockerfile: Dockerfile - args: - FMSGID_REF: ${FMSGID_REF:-main} - CACHEBUST: ${CACHEBUST:-} - restart: unless-stopped - environment: - GIN_MODE: ${GIN_MODE:-release} - FMSGID_PORT: ${FMSGID_PORT:-8080} - FMSGID_CSV_FILE: /opt/fmsgid/data/addresses.csv - PGHOST: postgres - PGPORT: 5432 - PGDATABASE: fmsgid - PGUSER: fmsgid_writer - PGPASSWORD: ${FMSGID_WRITER_PGPASSWORD} - PGSSLMODE: disable - volumes: - - fmsgid_data:/opt/fmsgid/data - depends_on: - postgres: - condition: service_healthy - - fmsgd: - build: - context: ../docker/fmsgd - dockerfile: Dockerfile - args: - FMSGD_REF: ${FMSGD_REF:-main} - CACHEBUST: ${CACHEBUST:-} - restart: unless-stopped - environment: - FMSG_DOMAIN: ${FMSG_DOMAIN} - FMSG_DATA_DIR: /opt/fmsg/data - FMSG_ID_URL: http://fmsgid:${FMSGID_PORT:-8080} - FMSG_SKIP_DOMAIN_IP_CHECK: ${FMSG_SKIP_DOMAIN_IP_CHECK:-false} - FMSG_SKIP_AUTHORISED_IPS: ${FMSG_SKIP_AUTHORISED_IPS:-false} - FMSG_TLS_CERT: /etc/letsencrypt/live/fmsg.${FMSG_DOMAIN}/fullchain.pem - FMSG_TLS_KEY: /etc/letsencrypt/live/fmsg.${FMSG_DOMAIN}/privkey.pem - PGHOST: postgres - PGPORT: 5432 - PGDATABASE: fmsgd - PGUSER: fmsgd_writer - PGPASSWORD: ${FMSGD_WRITER_PGPASSWORD} - PGSSLMODE: disable - volumes: - - fmsg_data:/opt/fmsg/data - - letsencrypt:/etc/letsencrypt:ro - ports: - - "${FMSG_PORT:-4930}:4930" - depends_on: - certbot: - condition: service_completed_successfully - postgres: - condition: service_healthy - fmsgid: - condition: service_started - - fmsg-webapi: - build: - context: ../docker/fmsg-webapi - dockerfile: Dockerfile - args: - FMSG_WEBAPI_REF: ${FMSG_WEBAPI_REF:-main} - CACHEBUST: ${CACHEBUST:-} - restart: unless-stopped - environment: +services: + + certbot: + image: docker.io/certbot/certbot + restart: "no" + entrypoint: ["/bin/sh", "/entrypoint.sh"] + environment: + FMSG_DOMAIN: ${FMSG_DOMAIN} + CERTBOT_EMAIL: ${CERTBOT_EMAIL} + volumes: + - letsencrypt:/etc/letsencrypt + - ../docker/certbot/entrypoint.sh:/entrypoint.sh:ro + ports: + - "80:80" + + postgres: + image: docker.io/library/postgres:18-alpine + restart: unless-stopped + environment: + POSTGRES_USER: ${PGUSER:-postgres} + POSTGRES_PASSWORD: ${PGPASSWORD} + POSTGRES_DB: postgres + FMSGD_WRITER_PGPASSWORD: ${FMSGD_WRITER_PGPASSWORD} + FMSGD_READER_PGPASSWORD: ${FMSGD_READER_PGPASSWORD} + FMSGID_WRITER_PGPASSWORD: ${FMSGID_WRITER_PGPASSWORD} + FMSGID_READER_PGPASSWORD: ${FMSGID_READER_PGPASSWORD} + volumes: + - postgres_data:/var/lib/postgresql + - ../docker/postgres/init:/docker-entrypoint-initdb.d:ro + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${PGUSER:-postgres}"] + interval: 10s + timeout: 5s + retries: 5 + + fmsgid: + build: + context: ../docker/fmsgid + dockerfile: Dockerfile + args: + FMSGID_REF: ${FMSGID_REF:-main} + CACHEBUST: ${CACHEBUST:-} + restart: unless-stopped + environment: + GIN_MODE: ${GIN_MODE:-release} + FMSGID_PORT: ${FMSGID_PORT:-8080} + FMSGID_CSV_FILE: /opt/fmsgid/data/addresses.csv + PGHOST: postgres + PGPORT: 5432 + PGDATABASE: fmsgid + PGUSER: fmsgid_writer + PGPASSWORD: ${FMSGID_WRITER_PGPASSWORD} + PGSSLMODE: disable + volumes: + - fmsgid_data:/opt/fmsgid/data + depends_on: + postgres: + condition: service_healthy + + fmsgd: + build: + context: ../docker/fmsgd + dockerfile: Dockerfile + args: + FMSGD_REF: ${FMSGD_REF:-main} + CACHEBUST: ${CACHEBUST:-} + restart: unless-stopped + environment: + FMSG_DOMAIN: ${FMSG_DOMAIN} + FMSG_DATA_DIR: /opt/fmsg/data + FMSG_ID_URL: http://fmsgid:${FMSGID_PORT:-8080} + FMSG_SKIP_DOMAIN_IP_CHECK: ${FMSG_SKIP_DOMAIN_IP_CHECK:-false} + FMSG_SKIP_AUTHORISED_IPS: ${FMSG_SKIP_AUTHORISED_IPS:-false} + FMSG_TLS_CERT: /etc/letsencrypt/live/fmsg.${FMSG_DOMAIN}/fullchain.pem + FMSG_TLS_KEY: /etc/letsencrypt/live/fmsg.${FMSG_DOMAIN}/privkey.pem + PGHOST: postgres + PGPORT: 5432 + PGDATABASE: fmsgd + PGUSER: fmsgd_writer + PGPASSWORD: ${FMSGD_WRITER_PGPASSWORD} + PGSSLMODE: disable + volumes: + - fmsg_data:/opt/fmsg/data + - letsencrypt:/etc/letsencrypt:ro + ports: + - "${FMSG_PORT:-4930}:4930" + depends_on: + certbot: + condition: service_completed_successfully + postgres: + condition: service_healthy + fmsgid: + condition: service_started + + fmsg-webapi: + build: + context: ../docker/fmsg-webapi + dockerfile: Dockerfile + args: + FMSG_WEBAPI_REF: ${FMSG_WEBAPI_REF:-main} + CACHEBUST: ${CACHEBUST:-} + restart: unless-stopped + environment: FMSG_DOMAIN: ${FMSG_DOMAIN} FMSG_ID_URL: http://fmsgid:${FMSGID_PORT:-8080} FMSG_API_TOKEN_ED25519_PRIVATE_KEY: ${FMSG_API_TOKEN_ED25519_PRIVATE_KEY:-} @@ -110,29 +110,29 @@ services: FMSG_JWT_ADDRESS_CLAIM: ${FMSG_JWT_ADDRESS_CLAIM:-} FMSG_API_PORT: ${FMSG_API_PORT:-8000} PGHOST: postgres - PGPORT: 5432 - PGDATABASE: fmsgd - PGUSER: fmsgd_writer - PGPASSWORD: ${FMSGD_WRITER_PGPASSWORD} - FMSG_TLS_CERT: /etc/letsencrypt/live/fmsgapi.${FMSG_DOMAIN}/fullchain.pem - FMSG_TLS_KEY: /etc/letsencrypt/live/fmsgapi.${FMSG_DOMAIN}/privkey.pem - FMSG_DATA_DIR: /opt/fmsg/data - PGSSLMODE: disable - volumes: - - fmsg_data:/opt/fmsg/data - - letsencrypt:/etc/letsencrypt:ro - ports: - - "443:8000" - depends_on: - certbot: - condition: service_completed_successfully - fmsgd: - condition: service_started - fmsgid: - condition: service_started - -volumes: - postgres_data: - fmsg_data: - fmsgid_data: - letsencrypt: + PGPORT: 5432 + PGDATABASE: fmsgd + PGUSER: fmsgd_writer + PGPASSWORD: ${FMSGD_WRITER_PGPASSWORD} + FMSG_TLS_CERT: /etc/letsencrypt/live/fmsgapi.${FMSG_DOMAIN}/fullchain.pem + FMSG_TLS_KEY: /etc/letsencrypt/live/fmsgapi.${FMSG_DOMAIN}/privkey.pem + FMSG_DATA_DIR: /opt/fmsg/data + PGSSLMODE: disable + volumes: + - fmsg_data:/opt/fmsg/data + - letsencrypt:/etc/letsencrypt:ro + ports: + - "443:8000" + depends_on: + certbot: + condition: service_completed_successfully + fmsgd: + condition: service_started + fmsgid: + condition: service_started + +volumes: + postgres_data: + fmsg_data: + fmsgid_data: + letsencrypt: diff --git a/docker/postgres/init/999-permissions.sql b/docker/postgres/init/999-permissions.sql index 77a4fcb..c12dab8 100644 --- a/docker/postgres/init/999-permissions.sql +++ b/docker/postgres/init/999-permissions.sql @@ -1,49 +1,49 @@ --- ============================================================= --- Permissions — runs last, after all objects exist. --- ============================================================= - --- ── fmsgd database ────────────────────────────────────────── - -\connect fmsgd - -REVOKE ALL ON DATABASE fmsgd FROM PUBLIC; -REVOKE ALL ON SCHEMA public FROM PUBLIC; - --- fmsgd_writer: read/write access -GRANT CONNECT ON DATABASE fmsgd TO fmsgd_writer; -GRANT USAGE ON SCHEMA public TO fmsgd_writer; -GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO fmsgd_writer; -ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO fmsgd_writer; -GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO fmsgd_writer; -ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO fmsgd_writer; - --- fmsgd_reader: read-only access -GRANT CONNECT ON DATABASE fmsgd TO fmsgd_reader; -GRANT USAGE ON SCHEMA public TO fmsgd_reader; -GRANT SELECT ON ALL TABLES IN SCHEMA public TO fmsgd_reader; -ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO fmsgd_reader; -GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO fmsgd_reader; -ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO fmsgd_reader; - --- ── fmsgid database ───────────────────────────────────────── - -\connect fmsgid - -REVOKE ALL ON DATABASE fmsgid FROM PUBLIC; -REVOKE ALL ON SCHEMA public FROM PUBLIC; - --- fmsgid_writer: read/write access -GRANT CONNECT ON DATABASE fmsgid TO fmsgid_writer; -GRANT USAGE ON SCHEMA public TO fmsgid_writer; -GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO fmsgid_writer; -ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO fmsgid_writer; -GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO fmsgid_writer; -ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO fmsgid_writer; - --- fmsgid_reader: read-only access -GRANT CONNECT ON DATABASE fmsgid TO fmsgid_reader; -GRANT USAGE ON SCHEMA public TO fmsgid_reader; -GRANT SELECT ON ALL TABLES IN SCHEMA public TO fmsgid_reader; -ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO fmsgid_reader; -GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO fmsgid_reader; -ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO fmsgid_reader; +-- ============================================================= +-- Permissions — runs last, after all objects exist. +-- ============================================================= + +-- ── fmsgd database ────────────────────────────────────────── + +\connect fmsgd + +REVOKE ALL ON DATABASE fmsgd FROM PUBLIC; +REVOKE ALL ON SCHEMA public FROM PUBLIC; + +-- fmsgd_writer: read/write access +GRANT CONNECT ON DATABASE fmsgd TO fmsgd_writer; +GRANT USAGE ON SCHEMA public TO fmsgd_writer; +GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO fmsgd_writer; +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO fmsgd_writer; +GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO fmsgd_writer; +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO fmsgd_writer; + +-- fmsgd_reader: read-only access +GRANT CONNECT ON DATABASE fmsgd TO fmsgd_reader; +GRANT USAGE ON SCHEMA public TO fmsgd_reader; +GRANT SELECT ON ALL TABLES IN SCHEMA public TO fmsgd_reader; +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO fmsgd_reader; +GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO fmsgd_reader; +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO fmsgd_reader; + +-- ── fmsgid database ───────────────────────────────────────── + +\connect fmsgid + +REVOKE ALL ON DATABASE fmsgid FROM PUBLIC; +REVOKE ALL ON SCHEMA public FROM PUBLIC; + +-- fmsgid_writer: read/write access +GRANT CONNECT ON DATABASE fmsgid TO fmsgid_writer; +GRANT USAGE ON SCHEMA public TO fmsgid_writer; +GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO fmsgid_writer; +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO fmsgid_writer; +GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO fmsgid_writer; +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO fmsgid_writer; + +-- fmsgid_reader: read-only access +GRANT CONNECT ON DATABASE fmsgid TO fmsgid_reader; +GRANT USAGE ON SCHEMA public TO fmsgid_reader; +GRANT SELECT ON ALL TABLES IN SCHEMA public TO fmsgid_reader; +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO fmsgid_reader; +GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO fmsgid_reader; +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO fmsgid_reader; From 9256cdf4f950c4d83154f3d71fa017f9d485c5b6 Mon Sep 17 00:00:00 2001 From: Mark Mennell Date: Mon, 13 Jul 2026 11:59:27 +0800 Subject: [PATCH 3/7] quickstart podman support --- QUICKSTART.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/QUICKSTART.md b/QUICKSTART.md index 238a8d6..2715e5c 100644 --- a/QUICKSTART.md +++ b/QUICKSTART.md @@ -13,17 +13,19 @@ Read the [README.md](https://github.com/markmnl/fmsg-docker) of this repo for mo 1. TCP port `4930` open to the internet (fmsg TLS) 2. TCP port `443` open to the internet (fmsg-webapi HTTPS) 3. TCP port `80` open to the internet (only first start - required for initial Let's Encrypt certificate issuance) -3. Docker and Docker Compose +3. Docker and Docker Compose, or Podman and podman-compose ## Steps ### 0. Server Setup -Clone this repository to the server and make sure docker is running. +Clone this repository to the server and make sure docker (or podman) is running. ``` git clone https://github.com/markmnl/fmsg-docker.git ``` +_NOTE_ This quickstart shows `docker compose` commands throughout. If you are using Podman instead, substitute `podman compose` (or `podman-compose`, depending on your install) for every `docker compose` command below. + ### 1. Configure DNS Create A (or AAAA if your public IP is IPv6) DNS records to resolve to your server IP for: From 3edb7d76a6ea1cd5a3704e6fbcf9f85740603209 Mon Sep 17 00:00:00 2001 From: Mark Mennell Date: Tue, 14 Jul 2026 21:21:59 +0800 Subject: [PATCH 4/7] fix-up quickstart --- .env.example | 36 ++++++++------- QUICKSTART.md | 89 ++++++++++++++++++++++++++++---------- compose/docker-compose.yml | 3 +- 3 files changed, 89 insertions(+), 39 deletions(-) diff --git a/.env.example b/.env.example index 762fd76..8c191da 100644 --- a/.env.example +++ b/.env.example @@ -1,13 +1,13 @@ -# ── Required ────────────────────────────────────────────── - -# fmsg domain name this host serves -# - fmsgd will uses TCP 4930 on fmsg. -# - fmsg-webapi uses HTTPS 443 on fmsgapi. -FMSG_DOMAIN=example.com - -# Email address for Let's Encrypt certificate registration -CERTBOT_EMAIL= - +# ── Required ────────────────────────────────────────────── + +# fmsg domain name this host serves +# - fmsgd will uses TCP 4930 on fmsg. +# - fmsg-webapi uses HTTPS 443 on fmsgapi. +FMSG_DOMAIN=example.com + +# Email address for Let's Encrypt certificate registration +CERTBOT_EMAIL= + # Base64-encoded Ed25519 seed or private key used by fmsg-webapi to mint # short-lived first-party JWTs from API keys. Generate with: # openssl rand -base64 32 @@ -16,15 +16,21 @@ FMSG_API_TOKEN_ED25519_PRIVATE_KEY=changeme # Per-service database passwords (used by application services) FMSGD_WRITER_PGPASSWORD=changeme FMSGID_WRITER_PGPASSWORD=changeme - -# ── Optional (defaults shown) ──────────────────────────── - -# FMSG_PORT=4930 +FMSGD_READER_PGPASSWORD=changeme +FMSGD_READER_PGPASSWORD=changeme + +# ── Optional (defaults shown) ──────────────────────────── + +# FMSG_PORT=4930 # FMSGID_PORT=8080 # GIN_MODE=release # FMSG_SKIP_DOMAIN_IP_CHECK=false -# External user JWT login (optional; set all four to enable JWKS auth) + +# External identity-provider JWT authentication (optional): configure these +# variables when fmsg-webapi should validate JWTs issued by an external +# identity provider through its JWKS endpoint. +# # FMSG_JWT_JWKS_URL=https://idp.example.com/.well-known/jwks.json # FMSG_JWT_ISSUER=https://idp.example.com/ # FMSG_JWT_AUDIENCE=fmsg-web-client diff --git a/QUICKSTART.md b/QUICKSTART.md index 2715e5c..8839421 100644 --- a/QUICKSTART.md +++ b/QUICKSTART.md @@ -1,8 +1,8 @@ # Quickstart - Setting up an fmsg host with fmsg-docker -This quickstart gets the docker compose stack from this repository up and running on your server. TLS provisioning is included and an HTTPS API is exposed so you can start sending and receiving fmsg messages for your domain. TCP port 4930 is also exposed for fmsg host-to-host communication. +This quickstart gets the docker compose stack from this repository up and running on your public fmsg server. -To learn more about fmsg, see the documentation repository: [fmsg](https://github.com/markmnl/fmsg). +TLS provisioning is included and an HTTPS API is exposed so you can start sending and receiving fmsg messages for your domain. TCP port 4930 is also exposed for fmsg host-to-host communication. Read the [README.md](https://github.com/markmnl/fmsg-docker) of this repo for more about settings and environment being used in this quickstart. @@ -17,14 +17,19 @@ Read the [README.md](https://github.com/markmnl/fmsg-docker) of this repo for mo ## Steps +_NOTE_ This quickstart uses `docker compose` throughout. If you are using Podman, replace each occurrence with `podman compose`. + ### 0. Server Setup -Clone this repository to the server and make sure docker (or podman) is running. -``` -git clone https://github.com/markmnl/fmsg-docker.git +Make sure Docker is installed and running. Create a dedicated non-root `fmsg` operator account, create its checkout location, and shallow-clone the repository's default branch: + +```sh +sudo useradd --create-home --shell /bin/bash fmsg +sudo install -d --owner=fmsg --group=fmsg /opt/fmsg-docker +sudo -u fmsg git clone --depth 1 https://github.com/markmnl/fmsg-docker.git /opt/fmsg-docker +sudo -u fmsg -H bash ``` -_NOTE_ This quickstart shows `docker compose` commands throughout. If you are using Podman instead, substitute `podman compose` (or `podman-compose`, depending on your install) for every `docker compose` command below. ### 1. Configure DNS @@ -37,9 +42,10 @@ _NOTE_ Ensure DNS is kept up-to-date with your server's IP so you can send and r ### 2. Configure FMSG -Copy the example env file: +As the `fmsg` user, configure the checkout in `/opt/fmsg-docker`. Copy the example env file: ```sh +cd /opt/fmsg-docker cp .env.example compose/.env ``` @@ -49,7 +55,9 @@ Edit `compose/.env` and set at least: FMSG_DOMAIN=example.com CERTBOT_EMAIL= FMSG_API_TOKEN_ED25519_PRIVATE_KEY= +FMSGD_READER_PGPASSWORD= FMSGD_WRITER_PGPASSWORD= +FMSGD_READER_PGPASSWORD= FMSGID_WRITER_PGPASSWORD= ``` @@ -59,21 +67,24 @@ _NOTE_ * Generate `FMSG_API_TOKEN_ED25519_PRIVATE_KEY` with `openssl rand -base64 32`. * For all secrets and passwords env vars create your own. -Start the stack for the first time from `compose/` and pass the one-time init passwords on the command line (keep these secret, keep them safe): -(might require sudo) +Exit the fmsg login shell and start the stack for the first time by changing into `/opt/fmsg-docker/compose` and passing the one-time postgres super user password on the command line. (Generate and keep PGPASSWORD yourself, this will only be needed first time running compose up). ```sh -cd compose -PGPASSWORD= \ -FMSGD_READER_PGPASSWORD= \ -FMSGID_READER_PGPASSWORD= \ -docker compose up -d +exit +cd /opt/fmsg-docker/compose +sudo env PGPASSWORD='' docker compose up -d +``` + +First time will take a few minutes to pull docker images and initalise the database. After than check everything started with: + +``` +sudo docker compose ps ``` If `fmsgd` is running and port `4930` is reachable on `fmsg.`, the host is up. -On first start, certbot will request Let's Encrypt TLS certificates for `fmsg.` and `fmsgapi.`. If certificate issuance fails (e.g. the domains do not resolve to the server or port 80 is blocked), the stack will not start. Certificates are persisted in a Docker volume and reused on subsequent starts. Once certificates are issued port 80 is no longer needed until certificates need to be renewed - usually 90 days. +On first start, certbot will request Let's Encrypt TLS certificates for `fmsg.` and `fmsgapi.`. If certificate issuance fails (e.g. the domains do not resolve to the server or port 80 is blocked by firewall, or already in use), the stack will not start. Certificates are persisted in a Docker volume and reused on subsequent starts. Once certificates are issued port 80 is no longer needed until certificates need to be renewed - usually 90 days. ## Next Steps @@ -90,24 +101,56 @@ address,display_name,accepting_new,limit_recv_size_total,limit_recv_size_per_msg You can copy it into the volume with (file changes will sync automatically): ```sh -docker compose cp addresses.csv fmsgid:/opt/fmsgid/data/addresses.csv +sudo docker compose cp addresses.csv fmsgid:/opt/fmsgid/data/addresses.csv ``` ### Connect a Client -Create or rotate an API key with the fmsg-webapi operator command, then use it with [fmsg-cli](https://github.com/markmnl/fmsg-cli): +Create an API key for a user, then use it with [fmsg-cli](https://github.com/markmnl/fmsg-cli) or access the [fmsg-webapi](https://github.com/markmnl/fmsg-webapi) API directly at `https://fmsgapi.`. + +To create an API key for `@alice@example.com`: ```sh -docker compose exec fmsg-webapi /opt/fmsg-webapi/fmsg-webapi api-key create-delegation \ +sudo docker compose exec fmsg-webapi /opt/fmsg-webapi/fmsg-webapi api-key create-delegation \ -owner @alice@example.com \ -agent cli \ -addr @alice@example.com \ - -cidr 203.0.113.0/24 \ - -expires 2026-12-31T00:00:00Z + -cidr 0.0.0.0/0,::/0 \ + -expires 2027-12-31T00:00:00Z + +``` + +The command prints the plaintext API key only once. Store it securely. The `owner` and `addr` are the same, so the key authenticates as `@alice@example.com`; it does not impersonate another user. The CIDR values permit connections from any IPv4 or IPv6 address. Restrict them when the deployment is ready for production use. + +Then use it from fmsg-cli: + +```sh +export FMSG_API_URL=https://fmsgapi. + +fmsg login -FMSG_API_URL=https://fmsgapi.example.com \ -FMSG_API_KEY=fmsgk__ \ fmsg list +fmsg send @recipient@example.com "Hello, world!" +fmsg send @recipient@example.com ./message.txt +echo "Hello via stdin" | fmsg send @recipient@example.com - ``` -The API key plaintext is printed once when created or rotated. Store it securely and pass it to automated clients through `FMSG_API_KEY`. +To use the API directly, exchange the API key for a short-lived JWT: + +```sh +export FMSG_API_URL=https://fmsgapi. +export FMSG_API_KEY=fmsgk__ + +curl --fail --silent --show-error \ + -X POST \ + -H "Authorization: Bearer $FMSG_API_KEY" \ + "$FMSG_API_URL/fmsg/token" +``` + +The response contains an `access_token`. Send it as a Bearer token with API requests: + +```sh +curl --fail --silent --show-error \ + -H "Authorization: Bearer " \ + "$FMSG_API_URL/fmsg" +``` \ No newline at end of file diff --git a/compose/docker-compose.yml b/compose/docker-compose.yml index a0d29e9..4661692 100644 --- a/compose/docker-compose.yml +++ b/compose/docker-compose.yml @@ -18,7 +18,8 @@ services: restart: unless-stopped environment: POSTGRES_USER: ${PGUSER:-postgres} - POSTGRES_PASSWORD: ${PGPASSWORD} + # Required by the image only while initializing an empty data volume. + POSTGRES_PASSWORD: ${PGPASSWORD:-} POSTGRES_DB: postgres FMSGD_WRITER_PGPASSWORD: ${FMSGD_WRITER_PGPASSWORD} FMSGD_READER_PGPASSWORD: ${FMSGD_READER_PGPASSWORD} From 8c292c07ed81e4a5f2ee2c9f5faae38983856456 Mon Sep 17 00:00:00 2001 From: Mark Mennell Date: Tue, 14 Jul 2026 21:26:20 +0800 Subject: [PATCH 5/7] words --- QUICKSTART.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/QUICKSTART.md b/QUICKSTART.md index 8839421..1f60c2f 100644 --- a/QUICKSTART.md +++ b/QUICKSTART.md @@ -120,11 +120,14 @@ sudo docker compose exec fmsg-webapi /opt/fmsg-webapi/fmsg-webapi api-key create ``` -The command prints the plaintext API key only once. Store it securely. The `owner` and `addr` are the same, so the key authenticates as `@alice@example.com`; it does not impersonate another user. The CIDR values permit connections from any IPv4 or IPv6 address. Restrict them when the deployment is ready for production use. +* The command prints the plaintext API key only once. Store it securely. +* `owner` and `addr` are the same, so the key authenticates as `@alice@example.com` in the above example. +* CIDR values: `-cidr 0.0.0.0/0,::/0` permit connections from any IPv4 or IPv6 address, restrict per your requirements. Then use it from fmsg-cli: ```sh +# save this export line to your ~/.bash_profile for future use export FMSG_API_URL=https://fmsgapi. fmsg login @@ -135,7 +138,8 @@ fmsg send @recipient@example.com ./message.txt echo "Hello via stdin" | fmsg send @recipient@example.com - ``` -To use the API directly, exchange the API key for a short-lived JWT: + +To use the API directly instead of fmsg-cli (which uses the API under the hood), exchange the API key for a short-lived JWT: ```sh export FMSG_API_URL=https://fmsgapi. From 269292c12aed821c40cecf844da1e2588e456843 Mon Sep 17 00:00:00 2001 From: Mark Mennell Date: Wed, 15 Jul 2026 20:21:36 +0800 Subject: [PATCH 6/7] scripts for run, stop and del local dev inc. podman --- scripts/del-local-dev.sh | 8 +- scripts/lib-container-engine.sh | 141 ++++++++++++++++++++++++++++++++ scripts/run-local-dev.sh | 21 +++-- scripts/stop-local-dev.sh | 6 +- scripts/update-dd.sh | 0 5 files changed, 163 insertions(+), 13 deletions(-) mode change 100644 => 100755 scripts/del-local-dev.sh create mode 100644 scripts/lib-container-engine.sh mode change 100644 => 100755 scripts/run-local-dev.sh mode change 100644 => 100755 scripts/stop-local-dev.sh mode change 100644 => 100755 scripts/update-dd.sh diff --git a/scripts/del-local-dev.sh b/scripts/del-local-dev.sh old mode 100644 new mode 100755 index a4dc2dc..3661a18 --- a/scripts/del-local-dev.sh +++ b/scripts/del-local-dev.sh @@ -36,6 +36,9 @@ compose_dir="$repo_root/compose" local_dev_dir="$repo_root/.bin/local-dev" local_override="$local_dev_dir/docker-compose.local-dev.yml" +# shellcheck source=lib-container-engine.sh +source "$script_dir/lib-container-engine.sh" + if [[ $# -gt 1 ]]; then usage exit 1 @@ -84,8 +87,7 @@ if [[ -f "$local_override" ]]; then compose_files+=(-f "$local_override") fi -require_command docker -docker compose version >/dev/null +select_container_engine cd "$compose_dir" @@ -99,7 +101,7 @@ rm -rf "$local_dev_dir" if docker network inspect fmsg-local >/dev/null 2>&1; then docker network rm fmsg-local >/dev/null 2>&1 || \ - echo "==> Shared Docker network fmsg-local is still in use; leaving it in place." + echo "==> Shared container network fmsg-local is still in use; leaving it in place." fi echo "==> Local fmsg stack deleted. Compose volumes and generated local files were removed." diff --git a/scripts/lib-container-engine.sh b/scripts/lib-container-engine.sh new file mode 100644 index 0000000..a3e3287 --- /dev/null +++ b/scripts/lib-container-engine.sh @@ -0,0 +1,141 @@ +#!/usr/bin/env bash +# Shared Docker-first / Podman-fallback support for local development scripts. + +select_container_engine() { + if command -v docker >/dev/null 2>&1 && docker compose version >/dev/null 2>&1; then + CONTAINER_ENGINE="docker" + CONTAINER_HOST_GATEWAY="host.docker.internal" + return + fi + + if ! command -v podman >/dev/null 2>&1; then + echo "Missing required container engine: install Docker Compose or Podman with podman-compose." >&2 + exit 1 + fi + + if ! command -v python3 >/dev/null 2>&1; then + echo "Missing required command for Podman Compose support: python3" >&2 + exit 1 + fi + + CONTAINER_ENGINE="podman" + CONTAINER_HOST_GATEWAY="host.containers.internal" + + container_engine_shim_dir="$(mktemp -d)" + trap 'rm -rf "$container_engine_shim_dir"' EXIT + export container_engine_shim_dir + + cat > "$container_engine_shim_dir/resolve-compose-vars.py" <<'EOF' +import os +import re +import sys + +PATTERN = re.compile(r'\$\{([A-Za-z_][A-Za-z0-9_]*)(:-([^}]*))?\}') + + +def replace_variable(match): + name, _, default = match.groups() + value = os.environ.get(name) + if value: + return value + return default if default is not None else '' + + +def resolve_variables(text): + return PATTERN.sub(replace_variable, text) + + +# podman-compose cannot parse mapping-form `depends_on` entries with a +# `condition` nested under `!override`, so collapse that form to a list. +depends_on_re = re.compile(r'^(\s*)depends_on:\s*!override\s*$') +key_re = re.compile(r'^(\s+)([A-Za-z0-9_.-]+):\s*$') +condition_re = re.compile(r'^\s+condition:\s*\S+\s*$') + + +def flatten_depends_on(text): + lines = text.split('\n') + output = [] + index = 0 + while index < len(lines): + match = depends_on_re.match(lines[index]) + if not match: + output.append(lines[index]) + index += 1 + continue + + base_indent = match.group(1) + index += 1 + services = [] + while index < len(lines): + key_match = key_re.match(lines[index]) + if key_match and len(key_match.group(1)) > len(base_indent): + services.append(key_match.group(2)) + index += 1 + while index < len(lines) and condition_re.match(lines[index]): + index += 1 + continue + break + output.append('{}depends_on: !override [{}]'.format(base_indent, ', '.join(services))) + return '\n'.join(output) + + +# Profiles on an override are not handled correctly by podman-compose. The +# local runner replaces certbot with a no-op entrypoint, so the profile is not +# needed when `--wait` is removed by the compatibility shim. +profiles_re = re.compile(r'^\s*profiles:\s*\[.*\]\s*$') + + +text = open(sys.argv[1], encoding='utf-8').read() +text = resolve_variables(text) +text = flatten_depends_on(text) +sys.stdout.write('\n'.join(line for line in text.split('\n') if not profiles_re.match(line))) +EOF + + cat > "$container_engine_shim_dir/docker" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail + +args=("$@") +if [[ "${1:-}" == "compose" ]]; then + for index in "${!args[@]}"; do + if [[ "${args[$index]}" == "cp" ]]; then + source_index=$((index + 1)) + destination_index=$((index + 2)) + destination="${args[$destination_index]}" + service="${destination%%:*}" + container_path="${destination#*:}" + container_name="${COMPOSE_PROJECT_NAME}_${service}_1" + exec podman cp "${args[$source_index]}" "${container_name}:${container_path}" + fi + done + + filtered=() + for index in "${!args[@]}"; do + if [[ "${args[$index]}" == "-f" ]]; then + next=$((index + 1)) + case "${args[$next]}" in + */docker-compose.local-dev.yml) + resolved="$container_engine_shim_dir/$(basename "${args[$next]}").$$.resolved.yml" + python3 "$container_engine_shim_dir/resolve-compose-vars.py" "${args[$next]}" > "$resolved" + args[$next]="$resolved" + ;; + esac + fi + + # podman-compose waits indefinitely for the no-op certbot container when + # Compose is invoked with `up --wait`. It also does not accept Docker + # Compose's `ps -a`; its `ps` command already includes stopped containers. + if [[ "${args[$index]}" == "--wait" || "${args[$index]}" == "-a" ]]; then + continue + fi + filtered+=("${args[$index]}") + done + args=("${filtered[@]}") +fi + +exec podman "${args[@]}" +EOF + chmod +x "$container_engine_shim_dir/docker" + + export PATH="$container_engine_shim_dir:$PATH" +} \ No newline at end of file diff --git a/scripts/run-local-dev.sh b/scripts/run-local-dev.sh old mode 100644 new mode 100755 index 70b087e..2424f91 --- a/scripts/run-local-dev.sh +++ b/scripts/run-local-dev.sh @@ -31,7 +31,7 @@ # fmsg-webapi validates JWTs using a separate local IdP. The issuer is # the host-facing URL (default http://localhost:8080). The JWKS URL must # be reachable from inside the fmsg-webapi container, so the default uses -# host.docker.internal rather than localhost. +# the selected container engine's host gateway rather than localhost. # ============================================================= set -euo pipefail @@ -49,9 +49,10 @@ Environment overrides: FMSG_PORT default: 4930 FMSG_WEBAPI_HOST_PORT default: 8181 POSTGRES_HOST_PORT default: 54321 - FMSG_JWT_JWKS_URL default: http://host.docker.internal:8080/.well-known/jwks.json + FMSG_JWT_JWKS_URL default: selected engine's host gateway on port 8080 IDP_JWT_ISSUER default: http://localhost:8080 FMSG_JWT_ISSUER default: value of IDP_JWT_ISSUER + FMSG_JWT_ADDRESS_CLAIM default: fmsg_address FMSG_CORS_ORIGINS default: http://localhost:8081 FMSG_ADDRESSES_CSV optional path/template copied to fmsgid addresses.csv FMSGD_WRITER_PGPASSWORD default: test @@ -96,6 +97,9 @@ local_dev_dir="$repo_root/.bin/local-dev" tls_dir="$local_dev_dir/tls" local_override="$local_dev_dir/docker-compose.local-dev.yml" +# shellcheck source=lib-container-engine.sh +source "$script_dir/lib-container-engine.sh" + if [[ $# -gt 2 ]]; then usage exit 1 @@ -120,6 +124,8 @@ if [[ "$domain" == fmsg.* || "$domain" == fmsgapi.* ]]; then exit 1 fi +select_container_engine + if [[ -n "$addresses_csv" && "$addresses_csv" != /* && ! "$addresses_csv" =~ ^[A-Za-z]: ]]; then addresses_csv="$initial_dir/$addresses_csv" fi @@ -131,7 +137,8 @@ export COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-fmsg_${sanitized_domain}}" export CERTBOT_EMAIL="${CERTBOT_EMAIL:-local-dev@${domain}}" export IDP_JWT_ISSUER="${IDP_JWT_ISSUER:-http://localhost:8080}" export FMSG_JWT_ISSUER="${FMSG_JWT_ISSUER:-$IDP_JWT_ISSUER}" -export FMSG_JWT_JWKS_URL="${FMSG_JWT_JWKS_URL:-http://host.docker.internal:8080/.well-known/jwks.json}" +export FMSG_JWT_JWKS_URL="${FMSG_JWT_JWKS_URL:-http://${CONTAINER_HOST_GATEWAY}:8080/.well-known/jwks.json}" +export FMSG_JWT_ADDRESS_CLAIM="${FMSG_JWT_ADDRESS_CLAIM:-fmsg_address}" export FMSG_CORS_ORIGINS="${FMSG_CORS_ORIGINS:-http://localhost:8081}" export FMSG_API_JWT_SECRET="${FMSG_API_JWT_SECRET:-}" export FMSG_PORT="${FMSG_PORT:-4930}" @@ -150,9 +157,6 @@ export FMSG_SKIP_DOMAIN_IP_CHECK="${FMSG_SKIP_DOMAIN_IP_CHECK:-true}" export FMSG_SKIP_AUTHORISED_IPS="${FMSG_SKIP_AUTHORISED_IPS:-true}" export FMSG_TLS_INSECURE_SKIP_VERIFY="${FMSG_TLS_INSECURE_SKIP_VERIFY:-true}" -require_command docker -docker compose version >/dev/null - cd "$compose_dir" compose_files=(-f docker-compose.yml) @@ -185,7 +189,7 @@ fi require_command openssl if ! docker network inspect fmsg-local >/dev/null 2>&1; then - echo "==> Creating shared Docker network: fmsg-local" + echo "==> Creating shared container network: fmsg-local" docker network create fmsg-local >/dev/null fi @@ -227,7 +231,7 @@ services: FMSG_TLS_KEY: /opt/fmsg/tls/fmsg.${FMSG_DOMAIN}.key FMSG_TLS_INSECURE_SKIP_VERIFY: "${FMSG_TLS_INSECURE_SKIP_VERIFY}" volumes: - - "${tls_dir}:/opt/fmsg/tls:ro" + - "${tls_dir}:/opt/fmsg/tls:ro,z" depends_on: !override postgres: condition: service_healthy @@ -264,6 +268,7 @@ services: - fmsgapi.${FMSG_DOMAIN} networks: + default: {} fmsg-local: external: true name: fmsg-local diff --git a/scripts/stop-local-dev.sh b/scripts/stop-local-dev.sh old mode 100644 new mode 100755 index fb5e75c..e704360 --- a/scripts/stop-local-dev.sh +++ b/scripts/stop-local-dev.sh @@ -32,6 +32,9 @@ repo_root="$(cd -- "$script_dir/.." && pwd)" compose_dir="$repo_root/compose" local_override="$repo_root/.bin/local-dev/docker-compose.local-dev.yml" +# shellcheck source=lib-container-engine.sh +source "$script_dir/lib-container-engine.sh" + if [[ $# -gt 1 ]]; then usage exit 1 @@ -80,8 +83,7 @@ if [[ -f "$local_override" ]]; then compose_files+=(-f "$local_override") fi -require_command docker -docker compose version >/dev/null +select_container_engine cd "$compose_dir" diff --git a/scripts/update-dd.sh b/scripts/update-dd.sh old mode 100644 new mode 100755 From e8ebc5e6756c25820e071bfaab21eeaa14da4acc Mon Sep 17 00:00:00 2001 From: Mark Mennell Date: Wed, 15 Jul 2026 20:21:57 +0800 Subject: [PATCH 7/7] cross ref md docs --- QUICKSTART.md | 9 ++++++++- README.md | 9 ++++++++- TMP_LOCAL_DEV.md => README_LOCAL_DEV.md | 11 +++++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) rename TMP_LOCAL_DEV.md => README_LOCAL_DEV.md (94%) diff --git a/QUICKSTART.md b/QUICKSTART.md index 1f60c2f..647e382 100644 --- a/QUICKSTART.md +++ b/QUICKSTART.md @@ -4,7 +4,14 @@ This quickstart gets the docker compose stack from this repository up and runnin TLS provisioning is included and an HTTPS API is exposed so you can start sending and receiving fmsg messages for your domain. TCP port 4930 is also exposed for fmsg host-to-host communication. -Read the [README.md](https://github.com/markmnl/fmsg-docker) of this repo for more about settings and environment being used in this quickstart. + +## Other Docs + +| Name | Description | +|--------------------------------------------|--------------------------------------------------------------------| +| [README.md](README.md) | Full README for this code repository. | +| [README_LOCAL_DEV.md](README_LOCAL_DEV.md) | Run the stack locally for development purposes. | + ## Requirements diff --git a/README.md b/README.md index 2415413..a84132b 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,14 @@ Dockerised stack composing a full fmsg setup including: [fmsgd](https://github.com/markmnl/fmsgd), [fmsgid](https://github.com/markmnl/fmsgid) and [fmsg-webapi](https://github.com/markmnl/fmsg-webapi) -Navigate to [QUICKSTART.md](QUICKSTART.md) + +## Other Docs + +| Name | Description | +|--------------------------------------------|--------------------------------------------------------------------| +| [QUICKSTART.md](QUICKSTART.md) | Get a production stack up and running on your server in minutes. | +| [README_LOCAL_DEV.md](README_LOCAL_DEV.md) | Run the stack locally for development purposes. | +
diff --git a/TMP_LOCAL_DEV.md b/README_LOCAL_DEV.md similarity index 94% rename from TMP_LOCAL_DEV.md rename to README_LOCAL_DEV.md index f8fa580..0fe428e 100644 --- a/TMP_LOCAL_DEV.md +++ b/README_LOCAL_DEV.md @@ -12,6 +12,17 @@ This is basically the same trick `test/docker-compose.test.yml` and `test/run-te use for integration tests (self-signed certs + `FMSG_SKIP_DOMAIN_IP_CHECK` + certbot disabled), just simplified to a single instance for interactive local dev. + +## Other Docs + +| Name | Description | +|--------------------------------------------|--------------------------------------------------------------------| +| [QUICKSTART.md](QUICKSTART.md) | Get a production stack up and running on your server in minutes. | +| [README.md](README.md) | Full README for this code repository. | + + +**NB:** Can run `./scripts/run-local.dev.sh` to perform the below + ## 1. Podman setup Docker Compose files work unmodified with Podman via the `podman-compose` project, or with