0771b401f3
docker compose down was silently failing (|| true) when the compose file was stale, leaving db/minio running on the old network subnet. The new postgrest container then couldn't bind because Docker tried to reuse the same subnet with conflicting container IPs. Now: rm -f all containers matching route_commerce_/route-commerce- by name first (works regardless of compose file state), then remove the network explicitly so Docker allocates a clean subnet on the next up. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
250 lines
12 KiB
YAML
250 lines
12 KiB
YAML
name: Deploy to route.crispygoat.com
|
|
|
|
on:
|
|
push:
|
|
branches:
|
|
- main
|
|
|
|
jobs:
|
|
deploy:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v4
|
|
with:
|
|
node-version: '22'
|
|
|
|
- name: Start Docker stack
|
|
env:
|
|
POSTGRES_USER: ${{ secrets.POSTGRES_USER }}
|
|
POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
|
|
POSTGRES_DB: ${{ secrets.POSTGRES_DB }}
|
|
MINIO_ROOT_USER: ${{ secrets.MINIO_ROOT_USER }}
|
|
MINIO_ROOT_PASSWORD: ${{ secrets.MINIO_ROOT_PASSWORD }}
|
|
POSTGREST_JWT_SECRET: ${{ secrets.POSTGREST_JWT_SECRET }}
|
|
run: |
|
|
APP_DIR=/home/tyler/route-commerce
|
|
mkdir -p $APP_DIR
|
|
|
|
# Free the dev-stack port (3001) and the port the previous deploy used
|
|
# (so a new deploy can pick it back up if it's the lowest free port)
|
|
PREV_PORT=$(cat .postgrest-port 2>/dev/null || echo "")
|
|
for port in 3001 $PREV_PORT; do
|
|
if [ -n "$port" ] && ss -tln 2>/dev/null | grep -qE "[[:space:]]127\.0\.0\.1:${port}[[:space:]]"; then
|
|
echo "Port $port in use, freeing..."
|
|
fuser -k -9 $port/tcp 2>/dev/null || true
|
|
docker ps -aq --filter "publish=$port" 2>/dev/null | xargs -r docker rm -f 2>/dev/null || true
|
|
fi
|
|
done
|
|
|
|
# Hard-stop the previous stack by name — works even if docker-compose.yml
|
|
# is missing or stale from a prior failed deploy.
|
|
docker ps -aq --filter "name=route_commerce_" --filter "name=route-commerce-" | xargs -r docker rm -f 2>/dev/null || true
|
|
docker compose -f $APP_DIR/docker-compose.yml down --remove-orphans 2>/dev/null || true
|
|
# Remove the network explicitly so Docker allocates a fresh subnet.
|
|
docker network rm route-commerce_default 2>/dev/null || true
|
|
sleep 3
|
|
|
|
# Verify all route-commerce containers are gone before picking a port.
|
|
REMAINING=$(docker ps -aq --filter "name=route_commerce_" --filter "name=route-commerce-" | wc -l)
|
|
if [ "$REMAINING" -gt 0 ]; then
|
|
echo "ERROR: $REMAINING route-commerce container(s) still running after teardown"
|
|
docker ps --filter "name=route_commerce_" --filter "name=route-commerce-"
|
|
exit 1
|
|
fi
|
|
|
|
# Find the first free host port starting from 3011. Persist the choice
|
|
# so the Build and Deploy steps below can use the same URL.
|
|
POSTGREST_HOST_PORT=3011
|
|
for attempt in 1 2 3 4 5 6 7 8 9 10; do
|
|
if ! ss -tln 2>/dev/null | grep -qE "[[:space:]]127\.0\.0\.1:${POSTGREST_HOST_PORT}[[:space:]]"; then
|
|
break
|
|
fi
|
|
echo "Port $POSTGREST_HOST_PORT in use, trying next... (attempt $attempt)"
|
|
POSTGREST_HOST_PORT=$((POSTGREST_HOST_PORT + 1))
|
|
if [ $POSTGREST_HOST_PORT -gt 30200 ]; then
|
|
echo "ERROR: no free port in 3011-30200 range"
|
|
exit 1
|
|
fi
|
|
sleep 1
|
|
done
|
|
echo "Using PostgREST host port: $POSTGREST_HOST_PORT"
|
|
echo "$POSTGREST_HOST_PORT" > .postgrest-port
|
|
export NEXT_PUBLIC_API_URL="http://localhost:$POSTGREST_HOST_PORT"
|
|
export POSTGREST_HOST_PORT
|
|
|
|
# Seed config files into APP_DIR if missing (they live in the repo, not in APP_DIR)
|
|
[ -f $APP_DIR/.env.example ] || cp .env.example $APP_DIR/.env.example
|
|
[ -f $APP_DIR/docker-compose.yml ] || cp docker-compose.yml $APP_DIR/docker-compose.yml
|
|
cd $APP_DIR
|
|
[ -f .env ] || cp .env.example .env
|
|
# Append production secrets to .env (overriding .env.example defaults)
|
|
{
|
|
echo "POSTGRES_USER=${POSTGRES_USER}"
|
|
echo "POSTGRES_PASSWORD=${POSTGRES_PASSWORD}"
|
|
echo "POSTGRES_DB=${POSTGRES_DB}"
|
|
echo "MINIO_ROOT_USER=${MINIO_ROOT_USER}"
|
|
echo "MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD}"
|
|
echo "POSTGREST_JWT_SECRET=${POSTGREST_JWT_SECRET}"
|
|
echo "POSTGREST_HOST_PORT=$POSTGREST_HOST_PORT"
|
|
echo "NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL"
|
|
} >> .env
|
|
# Bring the stack up fresh — --force-recreate ensures no stale
|
|
# network/container references from prior failed attempts
|
|
docker compose up -d --force-recreate db postgrest minio minio_init
|
|
# Wait for Postgres healthcheck
|
|
for i in $(seq 1 30); do
|
|
if docker compose exec -T db pg_isready -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" > /dev/null 2>&1; then
|
|
echo "Postgres is ready"
|
|
break
|
|
fi
|
|
sleep 2
|
|
done
|
|
|
|
- name: Apply migrations
|
|
env:
|
|
POSTGRES_USER: ${{ secrets.POSTGRES_USER }}
|
|
POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
|
|
POSTGRES_DB: ${{ secrets.POSTGRES_DB }}
|
|
run: |
|
|
APP_DIR=/home/tyler/route-commerce
|
|
# Seed supabase/ into APP_DIR if missing (the deploy step copies it after, but
|
|
# we need it here for migrations)
|
|
[ -d $APP_DIR/supabase ] || cp -r supabase $APP_DIR/supabase
|
|
cd $APP_DIR
|
|
PGPASSWORD="${POSTGRES_PASSWORD}" psql -h 127.0.0.1 -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" -v ON_ERROR_STOP=0 -f supabase/migrations/000_preflight_supabase_compat.sql || true
|
|
[ -f supabase/captured_schema.sql ] && PGPASSWORD="${POSTGRES_PASSWORD}" psql -h 127.0.0.1 -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" -v ON_ERROR_STOP=0 -f supabase/captured_schema.sql || echo "captured_schema.sql not present, skipping"
|
|
for f in supabase/migrations/[0-9]*.sql; do
|
|
PGPASSWORD="${POSTGRES_PASSWORD}" psql -h 127.0.0.1 -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" -v ON_ERROR_STOP=0 -q -f "$f" || echo "FAIL: $f"
|
|
done
|
|
|
|
- name: Install dependencies
|
|
run: npm install
|
|
|
|
- name: Build
|
|
env:
|
|
NODE_ENV: production
|
|
DATABASE_URL: ${{ secrets.DATABASE_URL }}
|
|
BETTER_AUTH_SECRET: ${{ secrets.BETTER_AUTH_SECRET }}
|
|
BETTER_AUTH_URL: ${{ secrets.BETTER_AUTH_URL }}
|
|
NEXT_PUBLIC_BETTER_AUTH_URL: ${{ secrets.NEXT_PUBLIC_BETTER_AUTH_URL }}
|
|
NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.NEXT_PUBLIC_SUPABASE_URL }}
|
|
NEXT_PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY }}
|
|
NEXT_PUBLIC_STORAGE_BASE_URL: ${{ secrets.NEXT_PUBLIC_STORAGE_BASE_URL }}
|
|
STORAGE_ENDPOINT: ${{ secrets.STORAGE_ENDPOINT }}
|
|
STORAGE_REGION: ${{ secrets.STORAGE_REGION }}
|
|
STORAGE_ACCESS_KEY: ${{ secrets.STORAGE_ACCESS_KEY }}
|
|
STORAGE_SECRET_KEY: ${{ secrets.STORAGE_SECRET_KEY }}
|
|
STORAGE_BUCKET_PREFIX: ${{ secrets.STORAGE_BUCKET_PREFIX }}
|
|
STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }}
|
|
STRIPE_WEBHOOK_SECRET: ${{ secrets.STRIPE_WEBHOOK_SECRET }}
|
|
STRIPE_PUBLISHABLE_KEY: ${{ secrets.STRIPE_PUBLISHABLE_KEY }}
|
|
RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }}
|
|
RESEND_WEBHOOK_SECRET: ${{ secrets.RESEND_WEBHOOK_SECRET }}
|
|
MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }}
|
|
MINIMAX_BASE_URL: ${{ secrets.MINIMAX_BASE_URL }}
|
|
FROM_EMAIL: ${{ secrets.FROM_EMAIL }}
|
|
run: |
|
|
POSTGREST_HOST_PORT=$(cat .postgrest-port)
|
|
export NEXT_PUBLIC_API_URL="http://localhost:$POSTGREST_HOST_PORT"
|
|
npm run build
|
|
|
|
- name: Deploy
|
|
env:
|
|
DATABASE_URL: ${{ secrets.DATABASE_URL }}
|
|
POSTGRES_USER: ${{ secrets.POSTGRES_USER }}
|
|
POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
|
|
POSTGRES_DB: ${{ secrets.POSTGRES_DB }}
|
|
MINIO_ROOT_USER: ${{ secrets.MINIO_ROOT_USER }}
|
|
MINIO_ROOT_PASSWORD: ${{ secrets.MINIO_ROOT_PASSWORD }}
|
|
POSTGREST_JWT_SECRET: ${{ secrets.POSTGREST_JWT_SECRET }}
|
|
BETTER_AUTH_SECRET: ${{ secrets.BETTER_AUTH_SECRET }}
|
|
BETTER_AUTH_URL: ${{ secrets.BETTER_AUTH_URL }}
|
|
NEXT_PUBLIC_BETTER_AUTH_URL: ${{ secrets.NEXT_PUBLIC_BETTER_AUTH_URL }}
|
|
STORAGE_ENDPOINT: ${{ secrets.STORAGE_ENDPOINT }}
|
|
STORAGE_REGION: ${{ secrets.STORAGE_REGION }}
|
|
STORAGE_ACCESS_KEY: ${{ secrets.STORAGE_ACCESS_KEY }}
|
|
STORAGE_SECRET_KEY: ${{ secrets.STORAGE_SECRET_KEY }}
|
|
STORAGE_BUCKET_PREFIX: ${{ secrets.STORAGE_BUCKET_PREFIX }}
|
|
NEXT_PUBLIC_STORAGE_BASE_URL: ${{ secrets.NEXT_PUBLIC_STORAGE_BASE_URL }}
|
|
PGRST_SERVER_PORT: ${{ secrets.PGRST_SERVER_PORT }}
|
|
PGRST_DB_URI: ${{ secrets.PGRST_DB_URI }}
|
|
PGRST_DB_ANON_ROLE: ${{ secrets.PGRST_DB_ANON_ROLE }}
|
|
PGRST_JWT_SECRET: ${{ secrets.POSTGREST_JWT_SECRET }}
|
|
NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.NEXT_PUBLIC_SUPABASE_URL }}
|
|
NEXT_PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY }}
|
|
STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }}
|
|
STRIPE_WEBHOOK_SECRET: ${{ secrets.STRIPE_WEBHOOK_SECRET }}
|
|
STRIPE_PUBLISHABLE_KEY: ${{ secrets.STRIPE_PUBLISHABLE_KEY }}
|
|
RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }}
|
|
RESEND_WEBHOOK_SECRET: ${{ secrets.RESEND_WEBHOOK_SECRET }}
|
|
MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }}
|
|
MINIMAX_BASE_URL: ${{ secrets.MINIMAX_BASE_URL }}
|
|
FROM_EMAIL: ${{ secrets.FROM_EMAIL }}
|
|
run: |
|
|
APP_DIR=/home/tyler/route-commerce
|
|
mkdir -p $APP_DIR
|
|
# Use the port chosen by Start Docker stack (persisted to .postgrest-port)
|
|
POSTGREST_HOST_PORT=$(cat .postgrest-port)
|
|
export NEXT_PUBLIC_API_URL="http://localhost:$POSTGREST_HOST_PORT"
|
|
|
|
# Write env file from secrets (preserves existing .env for docker compose)
|
|
{
|
|
printf "DATABASE_URL=%s\n" "$DATABASE_URL"
|
|
printf "NEXT_PUBLIC_API_URL=%s\n" "$NEXT_PUBLIC_API_URL"
|
|
printf "POSTGRES_USER=%s\n" "$POSTGRES_USER"
|
|
printf "POSTGRES_PASSWORD=%s\n" "$POSTGRES_PASSWORD"
|
|
printf "POSTGRES_DB=%s\n" "$POSTGRES_DB"
|
|
printf "MINIO_ROOT_USER=%s\n" "$MINIO_ROOT_USER"
|
|
printf "MINIO_ROOT_PASSWORD=%s\n" "$MINIO_ROOT_PASSWORD"
|
|
printf "POSTGREST_JWT_SECRET=%s\n" "$POSTGREST_JWT_SECRET"
|
|
printf "BETTER_AUTH_SECRET=%s\n" "$BETTER_AUTH_SECRET"
|
|
printf "BETTER_AUTH_URL=%s\n" "$BETTER_AUTH_URL"
|
|
printf "NEXT_PUBLIC_BETTER_AUTH_URL=%s\n" "$NEXT_PUBLIC_BETTER_AUTH_URL"
|
|
printf "STORAGE_ENDPOINT=%s\n" "$STORAGE_ENDPOINT"
|
|
printf "STORAGE_REGION=%s\n" "$STORAGE_REGION"
|
|
printf "STORAGE_ACCESS_KEY=%s\n" "$STORAGE_ACCESS_KEY"
|
|
printf "STORAGE_SECRET_KEY=%s\n" "$STORAGE_SECRET_KEY"
|
|
printf "STORAGE_BUCKET_PREFIX=%s\n" "$STORAGE_BUCKET_PREFIX"
|
|
printf "NEXT_PUBLIC_STORAGE_BASE_URL=%s\n" "$NEXT_PUBLIC_STORAGE_BASE_URL"
|
|
printf "PGRST_SERVER_PORT=%s\n" "$PGRST_SERVER_PORT"
|
|
printf "PGRST_DB_URI=%s\n" "$PGRST_DB_URI"
|
|
printf "PGRST_DB_ANON_ROLE=%s\n" "$PGRST_DB_ANON_ROLE"
|
|
printf "PGRST_JWT_SECRET=%s\n" "$POSTGREST_JWT_SECRET"
|
|
printf "NEXT_PUBLIC_SUPABASE_URL=%s\n" "$NEXT_PUBLIC_SUPABASE_URL"
|
|
printf "NEXT_PUBLIC_SUPABASE_ANON_KEY=%s\n" "$NEXT_PUBLIC_SUPABASE_ANON_KEY"
|
|
printf "STRIPE_SECRET_KEY=%s\n" "$STRIPE_SECRET_KEY"
|
|
printf "STRIPE_WEBHOOK_SECRET=%s\n" "$STRIPE_WEBHOOK_SECRET"
|
|
printf "STRIPE_PUBLISHABLE_KEY=%s\n" "$STRIPE_PUBLISHABLE_KEY"
|
|
printf "RESEND_API_KEY=%s\n" "$RESEND_API_KEY"
|
|
printf "RESEND_WEBHOOK_SECRET=%s\n" "$RESEND_WEBHOOK_SECRET"
|
|
printf "MINIMAX_API_KEY=%s\n" "$MINIMAX_API_KEY"
|
|
printf "MINIMAX_BASE_URL=%s\n" "$MINIMAX_BASE_URL"
|
|
printf "FROM_EMAIL=%s\n" "$FROM_EMAIL"
|
|
} > $APP_DIR/.env.production
|
|
|
|
# Copy build output and required files
|
|
rsync -a --delete .next/ $APP_DIR/.next/
|
|
rsync -a --delete public/ $APP_DIR/public/
|
|
cp package.json $APP_DIR/
|
|
cp docker-compose.yml $APP_DIR/
|
|
cp -r supabase/ $APP_DIR/
|
|
cp next.config.ts $APP_DIR/ 2>/dev/null || cp next.config.js $APP_DIR/ 2>/dev/null || true
|
|
|
|
# Install production deps only
|
|
cd $APP_DIR
|
|
npm install --omit=dev
|
|
|
|
# Start or restart PM2 process
|
|
if pm2 describe route-commerce > /dev/null 2>&1; then
|
|
pm2 restart route-commerce
|
|
else
|
|
pm2 start npm --name route-commerce -- start -- -p 3100
|
|
pm2 save
|
|
fi
|
|
|
|
echo "Deployed successfully"
|