Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.git
node_modules
.env
.env.local
.vite
tmp
*.log
ponder.log
deployment/.env
generated
22 changes: 22 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,28 @@ MAINNET_RPC_URL=https://eth.api.pocket.network
# Database (optional — defaults to Ponder's built-in SQLite for dev)
# Set this to use PostgreSQL from docker-compose.yml
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/programmatic-orders
# Schema for this app (required when using Postgres; avoids "previously used by a different Ponder app")
DATABASE_SCHEMA=programmatic_orders

# Dev/local: reduce RPC usage during sync (set to 1 to disable)
# DISABLE_REMOVAL_POLL=true — skip multicall singleOrders (RemovalPoller) every N blocks
# DISABLE_SETTLEMENT_FACTORY_CHECK=true — skip getCode + FACTORY() calls in the GPv2Settlement:Trade
# handler entirely. Use to benchmark base sync throughput vs. the cost of those RPC calls.


# Logging (optional)
# PINO_LOG_LEVEL=info

# ============================================================
# Production deployment (deployment/.env on remote machine)
# ============================================================
PROJECT_PREFIX=cow-programmatic
POSTGRES_USER=cow_programmatic
POSTGRES_PASSWORD=<change-me>
POSTGRES_DB=cow_programmatic
POSTGRES_PORT=5433
POSTGRES_MEMORY_LIMIT=1G
PONDER_EXPOSED_PORT=40000
PONDER_MEMORY_LIMIT=2G
# DATABASE_SCHEMA is injected automatically by manage.sh as:
# programmatic_orders_<git-sha> — do not set here
52 changes: 52 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: Deploy to Production

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

on:
workflow_dispatch:
push:
branches:
- main

jobs:
deploy:
name: Deploy to Production
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up SSH key
run: |
mkdir -p ~/.ssh
echo "${{ secrets.DEPLOY_SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519

- name: Set up SSH config
run: |
cat <<EOF > ~/.ssh/config
Host cow-deploy
HostName ${{ secrets.DEPLOY_SERVER_HOST }}
User ${{ secrets.DEPLOY_SERVER_USER }}
Port 22
IdentityFile ~/.ssh/id_ed25519
StrictHostKeyChecking no
ServerAliveInterval 30
ServerAliveCountMax 10
TCPKeepAlive yes
EOF
chmod 600 ~/.ssh/config

- name: Create .env file
run: |
echo "${{ secrets.DEPLOY_ENV_FILE_CONTENT }}" > .env

- name: Run deploy script
run: |
cd deployment
bash deploy-remotely.sh \
cow-deploy:${{ secrets.DEPLOY_PATH }} \
../.env
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ yarn-error.log*
.DS_Store

# Env files
.env
.env.local
deployment/.env

# Ponder
/generated/
Expand Down
38 changes: 38 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
FROM node:22-alpine AS base

ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN npm install -g pnpm@10

WORKDIR /usr/src/app

# ---- build stage ----
FROM base AS build

COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile

COPY . .

# ---- production image ----
FROM base

RUN apk add --no-cache curl

ENV NODE_ENV=production

COPY --from=build /usr/src/app ./
RUN pnpm install --frozen-lockfile

HEALTHCHECK \
--start-period=24h \
--start-interval=1s \
--retries=3 \
CMD curl -f http://localhost:3000/ready || exit 1

EXPOSE 3000/tcp

CMD ["pnpm", "start"]

ARG PIPELINE_BUILD_TAG="unknown"
ENV APP_REVISION=$PIPELINE_BUILD_TAG
53 changes: 53 additions & 0 deletions deployment/deploy-remotely.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#!/usr/bin/env bash
set -exo pipefail

REPO_ROOT_DIR=$(git rev-parse --show-toplevel)
APP_REVISION=$(git rev-parse --short HEAD)

DEPLOY_TARGET="${1:-}"
ENV_FILE_PATH="${2:-.env}"

if [[ -z "$DEPLOY_TARGET" ]]; then
echo "Usage: $0 <deploy_target> [env_file_path]"
exit 1
fi

if [[ "$DEPLOY_TARGET" == "-" ]]; then
# Local deployment
TARGET_DEPLOY_DIR="$REPO_ROOT_DIR"
APP_DEPLOY_DIR="$TARGET_DEPLOY_DIR/deployment"

bash "$APP_DEPLOY_DIR/manage.sh" ${MANAGE_CMD_OVERRIDE:-up} \
--env-file "$ENV_FILE_PATH" \
--revision "$APP_REVISION"
elif [[ "$DEPLOY_TARGET" =~ ^[^:]+:.+ ]]; then
# Remote deployment via SSH
SSH_HOST=$(echo "$DEPLOY_TARGET" | cut -d':' -f1)
REMOTE_PATH=$(echo "$DEPLOY_TARGET" | cut -d':' -f2-)

# Sync repository to remote
# .env is excluded — copied separately via scp to preserve server secrets
rsync -avz --delete \
--mkpath \
--exclude='.git' \
--exclude='node_modules' \
--exclude='.env' \
--exclude='.env.local' \
--exclude='.vite' \
--exclude='*.log' \
--exclude='tmp/' \
"$REPO_ROOT_DIR/" "$SSH_HOST:$REMOTE_PATH/"

# Copy .env to deployment directory on remote (separate from rsync)
REMOTE_ENV_PATH="$REMOTE_PATH/deployment/.env"
scp "$ENV_FILE_PATH" "$SSH_HOST:$REMOTE_ENV_PATH"

APP_DEPLOY_DIR="$REMOTE_PATH/deployment"
MANAGE_CMD="${MANAGE_CMD_OVERRIDE:-up}"

# Run manage.sh on remote
ssh "$SSH_HOST" "cd $APP_DEPLOY_DIR && bash manage.sh $MANAGE_CMD --env-file .env --revision $APP_REVISION"
else
echo "Error: <deploy_target> must be '-' or SSH_HOST:PATH"
exit 1
fi
51 changes: 51 additions & 0 deletions deployment/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
services:
postgres:
image: postgres:16
restart: unless-stopped
command: ["bash", "/start-db.sh"]
environment:
POSTGRES_DB: ${POSTGRES_DB:?error}
POSTGRES_USER: ${POSTGRES_USER:?error}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?error}
POSTGRES_MEMORY_LIMIT: ${POSTGRES_MEMORY_LIMIT:-1G}
POSTGRES_INITDB_ARGS: "--encoding=UTF8 --locale=en_US.UTF-8"
shm_size: 256m
ports:
- "${POSTGRES_PORT:-5433}:5432"
volumes:
- ./static/start-db.sh:/start-db.sh:ro
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5

ponder:
image: ${PROJECT_PREFIX:?error}-ponder:${APP_REVISION:?error}
restart: unless-stopped
build:
context: ..
dockerfile: Dockerfile
args:
PIPELINE_BUILD_TAG: ${APP_REVISION}
environment:
DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}
DATABASE_SCHEMA: ${DATABASE_SCHEMA:?error}
MAINNET_RPC_URL: ${MAINNET_RPC_URL:?error}
DISABLE_REMOVAL_POLL: ${DISABLE_REMOVAL_POLL:-false}
DISABLE_SETTLEMENT_FACTORY_CHECK: ${DISABLE_SETTLEMENT_FACTORY_CHECK:-false}
ports:
- "${PONDER_EXPOSED_PORT:-40000}:3000"
depends_on:
postgres:
condition: service_healthy
logging:
driver: json-file
options:
max-size: "50m"
max-file: "5"

volumes:
postgres-data:
name: ${PROJECT_PREFIX:?error}-postgres-data
93 changes: 93 additions & 0 deletions deployment/manage.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
#!/usr/bin/env bash
set -euo pipefail

usage() {
cat <<EOF
Usage: $0 <command> [options]

Commands:
up Deploy the stack
down Tear down the stack

Options:
-e, --env-file <path> Path to .env file (required)
-r, --revision <rev> Application revision (required for 'up')
-h, --help Show this help message
EOF
exit 1
}

COMMAND="${1:-}"
shift || true

ENV_FILE_PATH=""
APP_REVISION=""

while [[ $# -gt 0 ]]; do
case "$1" in
-e|--env-file) ENV_FILE_PATH="$2"; shift 2 ;;
-r|--revision) APP_REVISION="$2"; shift 2 ;;
-h|--help) usage ;;
*) echo "Unknown option: $1"; usage ;;
esac
done

if [[ -z "$COMMAND" ]]; then echo "Error: command required (up|down)"; usage; fi
if [[ -z "$ENV_FILE_PATH" ]]; then echo "Error: --env-file required"; usage; fi

APP_DEPLOY_DIR="$(dirname "$(realpath "$0")")"
cd "$APP_DEPLOY_DIR"

set -a
source "$ENV_FILE_PATH"
set +a

if [[ -z "${PROJECT_PREFIX:-}" ]]; then
echo "Error: PROJECT_PREFIX must be set in the env file"
exit 1
fi

export PROJECT_PREFIX
export APP_REVISION="${APP_REVISION:-latest}"
export DATABASE_SCHEMA="programmatic_orders"

cmd_up() {
if [[ -z "${APP_REVISION:-}" || "$APP_REVISION" == "latest" ]]; then
echo "Error: --revision is required for 'up'"
exit 1
fi

echo ">>> Building ponder image..."
docker compose \
-p "${PROJECT_PREFIX}" -f docker-compose.yml \
build --no-cache

echo ">>> Deploying (DATABASE_SCHEMA=${DATABASE_SCHEMA})..."
docker compose \
-p "${PROJECT_PREFIX}" -f docker-compose.yml \
up -d --remove-orphans

echo ">>> Cleaning up old ponder images..."
IMAGE_NAME="${PROJECT_PREFIX}-ponder"
OLD_IMAGES=$(docker images --format "{{.Repository}}:{{.Tag}}" "$IMAGE_NAME" | grep -v ":${APP_REVISION}$" || true)
if [[ -n "$OLD_IMAGES" ]]; then
echo "$OLD_IMAGES" | xargs -r docker rmi 2>/dev/null || true
fi
docker image prune -f 2>/dev/null || true
docker container prune -f 2>/dev/null || true

echo ">>> Deploy complete."
}

cmd_down() {
echo ">>> Stopping stack..."
docker compose \
-p "${PROJECT_PREFIX}" -f docker-compose.yml \
down -v --remove-orphans || true
}

case "$COMMAND" in
up) cmd_up ;;
down) cmd_down ;;
*) echo "Unknown command: $COMMAND"; usage ;;
esac
36 changes: 36 additions & 0 deletions deployment/static/start-db.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#!/usr/bin/env bash

POSTGRES_MAX_CONNECTIONS="${POSTGRES_MAX_CONNECTIONS:-100}"

if [ -n "${POSTGRES_MEMORY_LIMIT:-}" ]; then
LIMIT_BYTES=$(numfmt --from=iec "${POSTGRES_MEMORY_LIMIT}" 2>/dev/null)
if [ -z "$LIMIT_BYTES" ] || [ "$LIMIT_BYTES" = "0" ]; then
echo "Error: Invalid POSTGRES_MEMORY_LIMIT value: $POSTGRES_MEMORY_LIMIT" >&2
exit 1
fi
TOTAL_RAM_MB=$((LIMIT_BYTES / 1024 / 1024))
else
TOTAL_RAM_KB=$(grep MemTotal /proc/meminfo | awk '{print $2}')
TOTAL_RAM_MB=$((TOTAL_RAM_KB / 1024))
fi

SHARED_BUFFERS_MB=$((TOTAL_RAM_MB * 20 / 100))
MAINTENANCE_WORK_MEM_MB=$((TOTAL_RAM_MB * 5 / 100))
EFFECTIVE_CACHE_SIZE_MB=$((TOTAL_RAM_MB / 2))
WORK_MEM_MB=$(( (TOTAL_RAM_MB * 25 / 100) / POSTGRES_MAX_CONNECTIONS ))

if [ "$WORK_MEM_MB" -lt 1 ]; then WORK_MEM_MB=1; fi
if [ "$SHARED_BUFFERS_MB" -lt 32 ]; then SHARED_BUFFERS_MB=32; fi
if [ "$MAINTENANCE_WORK_MEM_MB" -lt 16 ]; then MAINTENANCE_WORK_MEM_MB=16; fi

set -x
exec docker-entrypoint.sh \
-c "max_connections=${POSTGRES_MAX_CONNECTIONS}" \
-c "shared_buffers=${SHARED_BUFFERS_MB}MB" \
-c "work_mem=${WORK_MEM_MB}MB" \
-c "maintenance_work_mem=${MAINTENANCE_WORK_MEM_MB}MB" \
-c "effective_cache_size=${EFFECTIVE_CACHE_SIZE_MB}MB" \
-c "max_wal_size=1GB" \
-c "min_wal_size=256MB" \
-c "checkpoint_completion_target=0.9" \
-c "wal_buffers=8MB"
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"type": "module",
"scripts": {
"dev": "ponder dev",
"start": "ponder start --schema ${DATABASE_SCHEMA:-public}",
"start": "ponder start -p 3000 --schema ${DATABASE_SCHEMA:-public}",
"db": "ponder db",
"codegen": "ponder codegen",
"lint": "eslint . --ext .ts",
Expand Down
Loading
Loading