Add production Docker Compose file for external PostgreSQL integration

- Introduced `docker-compose.prod.yml` for production deployment.
- Configured service to connect to an external PostgreSQL instance.
- Set environment variables for JWT and database connection strings.
- Defined network and volume for data protection keys.
This commit is contained in:
Matt Batchelder
2026-03-26 19:34:12 -04:00
parent 9a35e40083
commit fc510b9b20
105 changed files with 11291 additions and 2589 deletions

106
deploy.sh
View File

@@ -1,15 +1,16 @@
#!/usr/bin/env bash
# deploy.sh — Build and deploy OTSSignsOrchestrator to a Docker Swarm manager over SSH
# deploy.sh — Sync source and build on a Docker Swarm manager over SSH
set -euo pipefail
# ── Configuration ─────────────────────────────────────────────────────────────
SWARM_HOST="${SWARM_HOST:-}" # e.g. user@10.0.0.1 or set via env
IMAGE_NAME="${IMAGE_NAME:-otssigns-orchestrator}"
IMAGE_NAME="${IMAGE_NAME:-ots-orchestrator}"
IMAGE_TAG="${IMAGE_TAG:-latest}"
STACK_NAME="${STACK_NAME:-otssigns}"
COMPOSE_FILE="docker-compose.yml"
REMOTE_DIR="${REMOTE_DIR:-/opt/otssigns}"
STACK_NAME="${STACK_NAME:-otsorchestrator-dev}"
COMPOSE_FILE="${COMPOSE_FILE:-docker-compose.yml}" # use docker-compose.prod.yml for prod
REMOTE_DIR="${REMOTE_DIR:-/stack/dev/orchestrator}"
ENV_FILE="${ENV_FILE:-.env}" # local .env file with secrets
FRESH_DEPLOY="${FRESH_DEPLOY:-0}" # set to 1 to tear down old stack+volumes before deploying
# ── Validate ──────────────────────────────────────────────────────────────────
if [[ -z "$SWARM_HOST" ]]; then
@@ -25,24 +26,68 @@ if [[ ! -f "$COMPOSE_FILE" ]]; then
fi
FULL_IMAGE="${IMAGE_NAME}:${IMAGE_TAG}"
BUILD_FLAGS=""
[[ "$FRESH_DEPLOY" == "1" ]] && BUILD_FLAGS="--no-cache"
echo "==> Deploying ${FULL_IMAGE} to swarm manager: ${SWARM_HOST}"
echo " Stack: ${STACK_NAME}"
echo " Remote dir: ${REMOTE_DIR}"
echo " Stack: ${STACK_NAME}"
echo " Compose: ${COMPOSE_FILE}"
echo " Remote dir: ${REMOTE_DIR}"
echo " Fresh: ${FRESH_DEPLOY}"
echo ""
# ── Step 1: Build the image locally ──────────────────────────────────────────
echo "==> [1/5] Building Docker image..."
docker build -t "${FULL_IMAGE}" .
# ── Step 0 (fresh only): Tear down existing stack and volumes ─────────────────
if [[ "$FRESH_DEPLOY" == "1" ]]; then
echo "==> [0/4] Tearing down existing stack '${STACK_NAME}'..."
ssh "${SWARM_HOST}" "
if docker stack ls --format '{{.Name}}' | grep -q '^${STACK_NAME}$'; then
docker stack rm ${STACK_NAME}
echo ' Waiting for services to stop...'
timeout=60
while docker service ls --filter name=${STACK_NAME} --format '{{.Name}}' 2>/dev/null | grep -q .; do
sleep 2
timeout=\$(( timeout - 2 ))
[[ \$timeout -le 0 ]] && break
done
# Wait for containers/tasks to fully exit so volumes are released
echo ' Waiting for containers to exit...'
timeout=30
while docker ps -q --filter name=${STACK_NAME} 2>/dev/null | grep -q .; do
sleep 2
timeout=\$(( timeout - 2 ))
[[ \$timeout -le 0 ]] && break
done
else
echo ' No existing stack found, skipping removal.'
fi
# Remove volumes created by the stack (stack-name prefix) or by docker compose
# (compose project-name prefix, which defaults to the remote directory basename).
echo ' Removing stack volumes...'
for vol in \$(docker volume ls --format '{{.Name}}' | grep -E '^(${STACK_NAME}_|orchestrator_)'); do
# Force-remove any containers still holding this volume before deleting it
containers=\$(docker ps -aq --filter volume=\"\$vol\" 2>/dev/null)
if [[ -n \"\$containers\" ]]; then
echo \" Force-removing containers holding volume \$vol...\"
echo \"\$containers\" | xargs docker rm -f 2>/dev/null || true
sleep 1
fi
docker volume rm \"\$vol\" && echo \" Removed volume: \$vol\" || echo \" WARNING: Could not remove volume: \$vol (may still be in use)\"
done
"
echo " Stack removed."
fi
# ── Step 2: Save and stream the image to the remote host ─────────────────────
echo "==> [2/5] Transferring image to ${SWARM_HOST}..."
docker save "${FULL_IMAGE}" | ssh "${SWARM_HOST}" "docker load"
# ── Step 3: Prepare the remote directory and upload compose file + env ────────
echo "==> [3/5] Uploading compose file and environment..."
# ── Step 1: Sync source to the remote host ───────────────────────────────────
echo "==> [1/4] Syncing source to ${SWARM_HOST}:${REMOTE_DIR}..."
ssh "${SWARM_HOST}" "mkdir -p ${REMOTE_DIR}"
scp "${COMPOSE_FILE}" "${SWARM_HOST}:${REMOTE_DIR}/docker-compose.yml"
rsync -az --delete \
--exclude 'bin/' \
--exclude 'obj/' \
--exclude 'node_modules/' \
--exclude '.git/' \
--exclude 'logs/' \
--exclude 'wwwroot/' \
./ "${SWARM_HOST}:${REMOTE_DIR}/"
if [[ -f "$ENV_FILE" ]]; then
scp "${ENV_FILE}" "${SWARM_HOST}:${REMOTE_DIR}/.env"
@@ -51,27 +96,26 @@ else
echo " WARNING: No .env file found at '${ENV_FILE}'. Secrets must already exist on the remote host."
fi
# ── Step 4: Apply EF Core migrations before deploying the new stack ───────────
# The app runs migrations on startup (UseMigrationsEndPoint / MigrateAsync in Program.cs).
# If you need explicit pre-deploy migrations, uncomment the block below and
# ensure the remote host can reach PostgreSQL directly.
#
# echo "==> [3b] Running EF Core migrations..."
# ssh "${SWARM_HOST}" "docker run --rm --env-file ${REMOTE_DIR}/.env \
# -e ConnectionStrings__OrchestratorDb='...' \
# ${FULL_IMAGE} dotnet OTSSignsOrchestrator.dll --migrate-only"
# ── Step 2: Build the image on the remote host ───────────────────────────────
echo "==> [2/4] Building Docker image on ${SWARM_HOST}..."
ssh "${SWARM_HOST}" "cd ${REMOTE_DIR} && docker build ${BUILD_FLAGS} -t ${FULL_IMAGE} ."
# ── Step 5: Deploy the stack ──────────────────────────────────────────────────
echo "==> [4/5] Deploying stack '${STACK_NAME}'..."
# ── Step 3: Deploy the stack ──────────────────────────────────────────────────
echo "==> [3/4] Deploying stack '${STACK_NAME}'..."
# docker stack deploy does NOT load .env automatically — use `docker compose config`
# to resolve all variable substitutions first, then pipe the rendered YAML to the deploy.
# -p ensures compose uses the stack name as the project name so volume names are consistent.
ssh "${SWARM_HOST}" "cd ${REMOTE_DIR} && \
docker compose -p ${STACK_NAME} --env-file .env -f ${COMPOSE_FILE} config | \
sed '/^name:/d; s/published: \"\([0-9]*\)\"/published: \1/g' | \
docker stack deploy \
--compose-file docker-compose.yml \
--compose-file - \
--with-registry-auth \
--prune \
${STACK_NAME}"
# ── Step 6: Verify rollout ───────────────────────────────────────────────────
echo "==> [5/5] Waiting for service to converge..."
# ── Step 4: Verify rollout ───────────────────────────────────────────────────
echo "==> [4/4] Waiting for service to converge..."
ssh "${SWARM_HOST}" "docker service ls --filter name=${STACK_NAME}"
echo ""