Skip to content

Build, Deploy & Release Recipe

A batteries-included blueprint for monorepo infrastructure: modular Makefiles, Docker organization, semantic versioning, Cloudflare deployment, and Terraform IaC. Use this to bootstrap any project with production-grade patterns.


Table of Contents

  1. Project Structure Overview
  2. Makefile Module System
  3. Docker Organization
  4. Scripts Architecture
  5. Release & Version Bumping
  6. Cloudflare Workers & Containers
  7. Terraform Infrastructure
  8. Complete Bootstrap Checklist

Project Structure Overview

project-root/
├── Makefile # Root loader - includes all make/*.mk modules
├── make/ # Modular Makefile targets (one per concern)
│ ├── app.mk # App build/run targets
│ ├── cloudflare.mk # CF Workers deployment
│ ├── dev.mk # Development server
│ ├── gpu-infra.mk # Terraform GPU infrastructure
│ ├── rag.mk # RAG system targets
│ ├── release.mk # Version bumping & releases
│ └── whitepaper.mk # Documentation compilation
│
├── apps/ # Application packages (monorepo style)
│ ├── main-app/
│ │ ├── package.json # Version source of truth
│ │ ├── CHANGELOG.md # Auto-generated by standard-version
│ │ └── ...
│ └── admin-app/
│
├── brain/ # Internal tooling packages
│ └── rag/ # RAG system with its own package.json
│
├── infra/ # All infrastructure code
│ ├── dockerfiles/ # Docker images (one folder per image)
│ │ ├── main-app/
│ │ │ ├── Dockerfile
│ │ │ └── base.dockerfile # Alternative/specialized Dockerfile
│ │ └── sidecar-service/
│ │ ├── Dockerfile
│ │ ├── package.json # Self-contained buildable package
│ │ ├── src/
│ │ └── tsconfig.json
│ │
│ ├── cloudflare/ # Cloudflare Workers (one folder per worker)
│ │ ├── containers-worker/ # Main app container orchestration
│ │ │ ├── wrangler.toml
│ │ │ ├── package.json
│ │ │ └── src/index.ts
│ │ ├── api-worker/ # API endpoints
│ │ ├── websocket-relay/ # WebSocket Durable Objects
│ │ └── README.md
│ │
│ ├── compose/ # Docker Compose files for local/atomic services
│ │ └── app.yml
│ │
│ └── terraform/ # Terraform modules
│ └── gpu-node/
│ ├── main.tf
│ ├── variables.tf
│ ├── outputs.tf
│ ├── versions.tf
│ ├── cloud-init.yaml.tpl
│ ├── env.example
│ └── terraform.tfvars.example
│
├── scripts/ # Shell scripts & tooling
│ ├── app-version.mjs # Extract version from package.json
│ ├── release-sync-versions.mjs # Sync versions across manifests
│ ├── simple-release.sh # Fallback release script
│ ├── setup-gpu-infra.sh # Interactive setup wizard
│ ├── validate-gpu-infra.sh # Infrastructure validation
│ │
│ ├── rag-*.sh # RAG command wrappers (thin shells)
│ │
│ └── whitepaper/ # Documentation compiler (self-contained)
│ ├── package.json
│ ├── compile.ts
│ ├── md2pdf.ts
│ ├── style.css
│ └── template.latex
│
├── docs/ # Documentation
├── artifacts/ # Build outputs (gitignored)
│
├── release-please-config.json # Optional: Google release-please
├── .release-please-manifest.json
└── package.json # Root package (minimal - workspace deps only)

Key Organization Principles

  1. One folder per concern - Each Dockerfile, Worker, Terraform module gets its own folder
  2. Self-contained builds - infra/dockerfiles/*/ can include source code for buildable sidecars
  3. Scripts as thin wrappers - Shell scripts delegate to actual implementations
  4. Infrastructure isolation - All infra code under infra/, never mixed with app code

Makefile Module System

Root Makefile Pattern

# Makefile
.SILENT:
SHELL := /bin/bash
.SHELLFLAGS := -eu -o pipefail -c
ROOT_DIR := $(abspath $(CURDIR))
APP_DIR := $(ROOT_DIR)/apps/main-app
MAKEFILES := $(wildcard make/*.mk)
.PHONY: help
help: ## Show all available targets
@if [ -z "$(MAKEFILES)" ]; then \
printf "No module targets loaded yet.\n"; \
else \
printf "Available targets:\n"; \
grep -hE '^[a-zA-Z0-9_.-]+:.*##' $(MAKEFILES) | awk 'BEGIN {FS = ":.*##"}; {printf " %-25s %s\n", $$1, $$2}'; \
fi
# Utility: Generate secrets
BYTES ?= 32
FORMAT ?= hex
.PHONY: make-key
make-key: ## Generate a secret key (BYTES=32, FORMAT=hex|base64|uuid)
@case "$(FORMAT)" in \
hex) openssl rand -hex $(BYTES) ;; \
base64) openssl rand -base64 $(BYTES) ;; \
uuid) uuidgen | tr '[:upper:]' '[:lower:]' ;; \
*) echo "Unknown format: $(FORMAT)" && exit 1 ;; \
esac
# Auto-include all modules
ifneq ($(strip $(MAKEFILES)),)
include $(MAKEFILES)
endif

Module Pattern

Each make/*.mk file follows this pattern:

make/example.mk
# Module-specific path variables
EXAMPLE_DIR := $(ROOT_DIR)/path/to/component
EXAMPLE_SCRIPTS := $(ROOT_DIR)/scripts
# Targets with ## for auto-documentation
.PHONY: example-build
example-build: ## Build the example component
@echo "Building..."
@cd $(EXAMPLE_DIR) && ./build.sh
.PHONY: example-deploy
example-deploy: example-build ## Deploy (chains to build first)
@echo "Deploying..."
@cd $(EXAMPLE_DIR) && ./deploy.sh
# Pass-through arguments pattern
EXAMPLE_ARGS := $(strip $(ARGS))
.PHONY: example-run
example-run: ## Run with arguments (ARGS="--flag value")
$(EXAMPLE_SCRIPTS)/example-run.sh $(EXAMPLE_ARGS)

Conventions

ConventionExample
Every target has ## Descriptionbuild: ## Build the app
Use @ prefix for silent execution@echo "message"
Chain dependencies left-to-rightdeploy: build test
Use $(ROOT_DIR) for absolute pathscd $(ROOT_DIR)/apps/...
Pass arguments via ARGS variablemake target ARGS="--flag"
Group related targets with comments# ═══ Section ═══

Docker Organization

Folder Structure Philosophy

infra/dockerfiles/
├── main-app/ # App container
│ ├── Dockerfile # Production multi-stage build
│ └── base.dockerfile # Alternative (e.g., for different base image)
│
└── sidecar-service/ # Self-contained service
├── Dockerfile
├── package.json # Its own dependencies
├── tsconfig.json
└── src/
└── index.ts # Source lives HERE, not in apps/

Why this structure?

  • Docker COPY paths are clean and predictable
  • Each image is self-contained (no cross-folder dependencies)
  • Sidecars can have their own package.json without polluting main app
  • Build context is minimal (just that folder)

Multi-Stage Dockerfile Pattern

1.7-labs
# infra/dockerfiles/main-app/Dockerfile
ARG NODE_IMAGE=node:20-slim
ARG APP_VERSION="0.0.0"
# ─────────────────────────────────────────────────────────────────
# Base: Shared settings
# ─────────────────────────────────────────────────────────────────
FROM ${NODE_IMAGE} AS base
WORKDIR /app
ENV NODE_ENV=production
RUN corepack enable
# ─────────────────────────────────────────────────────────────────
# Deps: Install all dependencies
# ─────────────────────────────────────────────────────────────────
FROM base AS deps
ENV NODE_ENV=development
COPY package.json yarn.lock ./
RUN --mount=type=cache,target=/root/.cache/yarn \
yarn install --frozen-lockfile --non-interactive
# ─────────────────────────────────────────────────────────────────
# Builder: Build the application
# ─────────────────────────────────────────────────────────────────
FROM base AS builder
ARG APP_VERSION
ENV PUBLIC_APP_VERSION=${APP_VERSION}
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN yarn build && yarn cache clean
# ─────────────────────────────────────────────────────────────────
# Prod-deps: Production dependencies only
# ─────────────────────────────────────────────────────────────────
FROM deps AS prod-deps
ENV NODE_ENV=production
RUN --mount=type=cache,target=/root/.cache/yarn \
yarn install --frozen-lockfile --production --prefer-offline
# ─────────────────────────────────────────────────────────────────
# Runner: Final minimal image
# ─────────────────────────────────────────────────────────────────
FROM ${NODE_IMAGE} AS runner
WORKDIR /app
ENV NODE_ENV=production \
HOST=0.0.0.0 \
PORT=8080
COPY package.json yarn.lock ./
COPY --from=prod-deps /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
EXPOSE 8080
USER node
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s \
CMD node -e "fetch('http://127.0.0.1:8080/_health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1));"
CMD ["node", "./dist/server/entry.mjs"]

Sidecar Dockerfile Pattern (Alpine, Self-Contained)

# infra/dockerfiles/sidecar-service/Dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY src ./src
COPY tsconfig.json ./
RUN npm run build
RUN npm ci --omit=dev
# ─────────────────────────────────────────────────────────────────
FROM node:20-alpine
WORKDIR /app
# Security: Non-root user
RUN addgroup -g 1001 -S appuser && \
adduser -S appuser -u 1001 -G appuser
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
HEALTHCHECK --interval=15s --timeout=5s --start-period=10s --retries=3 \
CMD wget -q --spider http://localhost:8080/health || exit 1
USER appuser
EXPOSE 8080
CMD ["node", "dist/index.js"]

Docker Compose for Development

infra/compose/app.yml
version: '3.8'
services:
main-app:
build:
context: ../dockerfiles/main-app
dockerfile: Dockerfile
ports:
- "8080:8080"
environment:
- NODE_ENV=development
volumes:
- ../../apps/main-app/src:/app/src:ro
healthcheck:
test: ["CMD", "curl", "-sf", "http://localhost:8080/_health"]
interval: 30s
timeout: 10s
retries: 3
sidecar:
build:
context: ../dockerfiles/sidecar-service
ports:
- "8081:8080"
environment:
- MAIN_APP_URL=http://main-app:8080
depends_on:
main-app:
condition: service_healthy

Scripts Architecture

Pattern: Thin Shell Wrappers

Scripts in the root scripts/ folder are thin wrappers that delegate to actual implementations:

scripts/rag-query.sh
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
yarn --cwd "$ROOT_DIR/brain/rag" rag:query "$@"

Why?

  • Makefile targets call these scripts
  • Scripts handle path resolution
  • Actual implementation lives in the package (brain/rag)
  • Easy to call from anywhere

Script Categories

CategoryPatternExample
WrappersDelegate to package scriptsrag-*.sh
UtilitiesStandalone toolsapp-version.mjs
Setup wizardsInteractive configurationsetup-gpu-infra.sh
ValidationPre-flight checksvalidate-gpu-infra.sh
SubsystemsSelf-contained toolingwhitepaper/ folder

Version Extraction Script

scripts/app-version.mjs
#!/usr/bin/env node
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
const __dirname = dirname(fileURLToPath(import.meta.url));
const pkgPath = join(__dirname, '..', 'apps', 'main-app', 'package.json');
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
if (!pkg.version) {
console.error('Unable to read version');
process.exit(1);
}
process.stdout.write(pkg.version);

Setup Wizard Pattern

scripts/setup-something.sh
#!/usr/bin/env bash
set -euo pipefail
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'
BOLD='\033[1m'
# Paths
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
# Helpers
print_header() { echo -e "\n${BOLD}${CYAN}═══ $1 ═══${NC}\n"; }
print_step() { echo -e "${GREEN}▶${NC} $1"; }
print_success() { echo -e "${GREEN}✓${NC} $1"; }
print_warning() { echo -e "${YELLOW}⚠${NC} $1"; }
print_error() { echo -e "${RED}✗${NC} $1"; }
check_command() {
if command -v "$1" &> /dev/null; then
print_success "$1 is installed"
return 0
else
print_error "$1 is not installed"
return 1
fi
}
# Main
main() {
print_header "Setup Wizard"
# Step 1: Check prerequisites
print_header "Checking Prerequisites"
check_command "docker" || exit 1
check_command "terraform" || exit 1
# Step 2: Collect input
print_header "Configuration"
read -p "Enter API token: " -s API_TOKEN
echo
# Step 3: Write config files
print_header "Writing Configuration"
cat > "$PROJECT_ROOT/infra/terraform/.env" << EOF
API_TOKEN=$API_TOKEN
EOF
print_success "Created .env file"
# Summary
print_header "Setup Complete"
echo "Next steps:"
echo " 1. make terraform-init"
echo " 2. make terraform-plan"
}
main "$@"

Validation Script Pattern

scripts/validate-something.sh
#!/usr/bin/env bash
set -euo pipefail
PASSED=0
FAILED=0
WARNINGS=0
check_pass() { echo -e "✓ $1"; PASSED=$((PASSED + 1)); }
check_fail() { echo -e "✗ $1"; FAILED=$((FAILED + 1)); }
check_warn() { echo -e "⚠ $1"; WARNINGS=$((WARNINGS + 1)); }
# Run checks
[[ -f "config.json" ]] && check_pass "Config exists" || check_fail "Config missing"
command -v docker &>/dev/null && check_pass "Docker installed" || check_fail "Docker not found"
# Summary
echo ""
echo "Passed: $PASSED | Failed: $FAILED | Warnings: $WARNINGS"
[[ $FAILED -eq 0 ]] && exit 0 || exit 1

Release & Version Bumping

Strategy: standard-version + Make Targets

The release workflow uses standard-version for:

  • Automatic version bumping based on conventional commits
  • CHANGELOG.md generation
  • Git tagging

Version source of truth: apps/main-app/package.json

Release Makefile Module

make/release.mk
RELEASE_SYNC_SCRIPT := $(ROOT_DIR)/scripts/release-sync-versions.mjs
APP_DIR := $(ROOT_DIR)/apps/main-app
# Post-release hook: sync versions across manifests
define post_release
@echo "📋 Syncing versions across manifest files..."
@node $(RELEASE_SYNC_SCRIPT) 2>/dev/null || true
@echo "📄 Copying CHANGELOG.md to public folder..."
@cp $(APP_DIR)/CHANGELOG.md $(APP_DIR)/public/CHANGELOG.md 2>/dev/null || true
@git add -A 2>/dev/null || true
@git commit --amend --no-edit 2>/dev/null || true
endef
# ─────────────────────────────────────────────────────────────────
# Main Release Commands
# ─────────────────────────────────────────────────────────────────
.PHONY: release
release: ## Create release (auto-determines version from commits)
@echo "🚀 Creating release..."
@cd $(APP_DIR) && npx standard-version
$(post_release)
@git push --follow-tags origin main
@echo "✅ Release pushed!"
.PHONY: release-patch
release-patch: ## Force patch release (x.y.Z)
@cd $(APP_DIR) && npx standard-version --release-as patch
$(post_release)
.PHONY: release-minor
release-minor: ## Force minor release (x.Y.0)
@cd $(APP_DIR) && npx standard-version --release-as minor
$(post_release)
.PHONY: release-major
release-major: ## Force major release (X.0.0)
@cd $(APP_DIR) && npx standard-version --release-as major
$(post_release)
.PHONY: release-dry
release-dry: ## Preview release (no changes)
@cd $(APP_DIR) && npx standard-version --dry-run
.PHONY: release-first
release-first: ## Create first release (when starting fresh)
@cd $(APP_DIR) && npx standard-version --first-release
$(post_release)
# ─────────────────────────────────────────────────────────────────
# Combined Commands
# ─────────────────────────────────────────────────────────────────
.PHONY: release-push
release-push: ## Push release commit and tags
@git push --follow-tags origin main
.PHONY: release-full
release-full: release ## Alias (release already pushes)

Version Sync Script

Keeps versions in sync across:

  • apps/main-app/package.json
  • .release-please-manifest.json
  • Any other manifests
scripts/release-sync-versions.mjs
#!/usr/bin/env node
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = join(__dirname, '..');
// Files to sync
const PACKAGE_JSON = join(ROOT, 'apps/main-app/package.json');
const MANIFEST = join(ROOT, '.release-please-manifest.json');
// Get current version
const pkg = JSON.parse(readFileSync(PACKAGE_JSON, 'utf8'));
const version = pkg.version;
console.log(`[sync] Version: ${version}`);
// Update manifest
if (existsSync(MANIFEST)) {
const manifest = JSON.parse(readFileSync(MANIFEST, 'utf8'));
manifest['apps/main-app'] = version;
writeFileSync(MANIFEST, JSON.stringify(manifest, null, 2) + '\n');
console.log(`[sync] Updated ${MANIFEST}`);
}

Fallback: Simple Release Script

For when you need to bypass standard-version:

scripts/simple-release.sh
#!/bin/bash
set -e
RELEASE_TYPE=${1:-patch}
APP_PATH="apps/main-app"
cd "$APP_PATH"
CURRENT=$(node -p "require('./package.json').version")
echo "Current: $CURRENT"
npm version $RELEASE_TYPE --no-git-tag-version
NEW=$(node -p "require('./package.json').version")
echo "New: $NEW"
cd ../..
git add "$APP_PATH/package.json"
git commit -m "chore: bump to v$NEW"
TAG="v$NEW"
git tag -a "$TAG" -m "Release $TAG"
git push origin HEAD
git push origin "$TAG"
echo "✅ Released $TAG"

Conventional Commits Quick Reference

PrefixVersion BumpExample
feat:Minorfeat: add dark mode
fix:Patchfix: resolve crash
feat!: or BREAKING CHANGE:Majorfeat!: new API
chore:, docs:, style:Nonedocs: update readme

Cloudflare Workers & Containers

Cloudflare Containers allow you to run Docker containers on Cloudflare’s edge network, managed by Durable Objects. This gives you:

  • Edge-deployed containers - Your Docker image runs close to users
  • Auto-scaling - Containers spin up/down based on demand
  • Sleep mode - Containers sleep after inactivity (cost savings)
  • Managed registry - Push directly to Cloudflare’s container registry

Architecture Overview

┌─────────────────────────────────────────────────────────────────┐
│ Cloudflare Edge │
│ ┌───────────────┐ ┌─────────────────────────────────┐ │
│ │ Worker │──────│ Container Durable Object │ │
│ │ (index.ts) │ │ ┌───────────────────────────┐ │ │
│ │ │ │ │ Your Docker Container │ │ │
│ │ • Routes │ │ │ (main-app:v1.0.0) │ │ │
│ │ • Health │ │ │ │ │ │
│ │ • Management │ │ │ PORT 8080 │ │ │
│ └───────────────┘ │ └───────────────────────────┘ │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘

Flow:

  1. Request hits Cloudflare Worker
  2. Worker gets/creates Container Durable Object instance
  3. DO manages container lifecycle (start, stop, sleep)
  4. Worker proxies request to container
  5. Container responds via Worker

Directory Structure

infra/cloudflare/
├── containers-worker/ # Main app container orchestration
│ ├── wrangler.toml # Wrangler + container config
│ ├── package.json
│ ├── tsconfig.json
│ └── src/
│ └── index.ts # Worker + Container DO class
│
├── api-worker/ # Separate API worker (optional)
├── websocket-relay/ # WebSocket DO (optional)
└── README.md

Deployment Pipeline

┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ 1. Build │───▶│ 2. Push │───▶│ 3. Sync │───▶│ 4. Deploy │
│ (Docker) │ │ (Registry) │ │ (wrangler) │ │ (Worker) │
└──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘
docker wrangler perl -pi wrangler
buildx containers push (update toml) deploy

make cf-deploy runs all 4 steps:

  1. Build Docker image locally with docker buildx
  2. Push to Cloudflare’s managed registry via wrangler containers push
  3. Update wrangler.toml with the new image tag
  4. Deploy the Worker which references the new image

Makefile Module

make/cloudflare.mk
CF_ROOT := $(ROOT_DIR)/infra/cloudflare
CF_WORKER_DIR := $(CF_ROOT)/containers-worker
CF_WRANGLER_CONFIG := $(CF_WORKER_DIR)/wrangler.toml
CF_DOCKER_CONTEXT := $(ROOT_DIR)/infra/dockerfiles/main-app
CF_DOCKERFILE := $(CF_DOCKER_CONTEXT)/Dockerfile
CF_DOCKER_PLATFORM := linux/amd64
CF_IMAGE_NAME := main-app
# Version from package.json (source of truth)
APP_VERSION := $(shell node $(ROOT_DIR)/scripts/app-version.mjs)
CF_IMAGE_TAG := v$(APP_VERSION)
# Cloudflare registry URL pattern:
# registry.cloudflare.com/<ACCOUNT_ID>/<IMAGE_NAME>:<TAG>
CF_REGISTRY := registry.cloudflare.com/YOUR_ACCOUNT_ID/$(CF_IMAGE_NAME)
CF_IMAGE_URI := $(CF_REGISTRY):$(CF_IMAGE_TAG)
# ═══════════════════════════════════════════════════════════════════
# Authentication
# ═══════════════════════════════════════════════════════════════════
.PHONY: cf-login
cf-login: ## Authenticate with Cloudflare (opens browser)
cd $(ROOT_DIR) && npx wrangler login
# ═══════════════════════════════════════════════════════════════════
# Container Build & Push
# ═══════════════════════════════════════════════════════════════════
.PHONY: cf-container-build
cf-container-build: ## Build and push via Wrangler (simpler, but less control)
cd $(ROOT_DIR) && npx wrangler containers build $(CF_DOCKER_CONTEXT) \
--tag $(CF_IMAGE_NAME):$(CF_IMAGE_TAG) --push
.PHONY: cf-container-build-local
cf-container-build-local: ## Build container locally with Docker (more control)
docker buildx build \
--platform $(CF_DOCKER_PLATFORM) \
-f $(CF_DOCKERFILE) \
-t $(CF_IMAGE_NAME):$(CF_IMAGE_TAG) \
--build-arg APP_VERSION=$(APP_VERSION) \
--load \
$(CF_DOCKER_CONTEXT)
.PHONY: cf-container-push
cf-container-push: ## Push local image to Cloudflare registry
cd $(ROOT_DIR) && npx wrangler containers push $(CF_IMAGE_NAME):$(CF_IMAGE_TAG)
.PHONY: cf-container-publish
cf-container-publish: cf-container-build-local cf-container-push ## Build locally + push
.PHONY: cf-container-images
cf-container-images: ## List images in Cloudflare registry
cd $(ROOT_DIR) && npx wrangler containers images list --filter $(CF_IMAGE_NAME)
# ═══════════════════════════════════════════════════════════════════
# Wrangler Config Sync
# ═══════════════════════════════════════════════════════════════════
.PHONY: cf-sync-wrangler-image
cf-sync-wrangler-image: ## Update wrangler.toml with latest image tag
@echo "Updating image references to $(CF_IMAGE_URI)"
@perl -0pi -e 's!image = "$(CF_REGISTRY):[^"]+"!image = "$(CF_IMAGE_URI)"!g' $(CF_WRANGLER_CONFIG)
# ═══════════════════════════════════════════════════════════════════
# Worker Deployment
# ═══════════════════════════════════════════════════════════════════
.PHONY: cf-worker-install
cf-worker-install: ## Install worker dependencies
cd $(CF_WORKER_DIR) && npm install
.PHONY: cf-worker-dev
cf-worker-dev: cf-worker-install ## Run worker locally (dev mode)
cd $(CF_WORKER_DIR) && npm run dev
.PHONY: cf-worker-deploy
cf-worker-deploy: cf-worker-install ## Deploy worker only (no container build)
cd $(ROOT_DIR) && npx wrangler deploy --config $(CF_WRANGLER_CONFIG) --env production
# ═══════════════════════════════════════════════════════════════════
# Full Deploy (The Main Command)
# ═══════════════════════════════════════════════════════════════════
.PHONY: cf-deploy
cf-deploy: cf-container-publish cf-sync-wrangler-image cf-worker-install ## Full deploy: build → push → sync → deploy
git checkout main && git pull
cd $(ROOT_DIR) && npx wrangler deploy --config $(CF_WRANGLER_CONFIG) --env production
@echo "✅ Deployed $(CF_IMAGE_URI) to production"

Worker Code Pattern

The Worker extends Container from @cloudflare/containers and handles routing:

infra/cloudflare/containers-worker/src/index.ts
import { Container, getContainer } from '@cloudflare/containers';
// Health check paths that return container state instead of proxying
const HEALTH_PATHS = new Set(['/_health', '/_healthz', '/_readyz']);
// Default instance name (can be overridden via env or query param)
const DEFAULT_INSTANCE = 'main-app-ssr';
const CONTAINER_PORT = 8080;
/**
* Container Durable Object class
* Extends Container to configure your specific container settings
*/
export class MainAppContainerDO extends Container {
// Port your container listens on
defaultPort = CONTAINER_PORT;
// Auto-sleep after 10 minutes of inactivity
sleepAfter = '10m';
// Environment variables passed to the container
envVars = {
NODE_ENV: 'production',
HOST: '0.0.0.0',
PORT: `${CONTAINER_PORT}`
};
}
interface Env {
// Durable Object binding (configured in wrangler.toml)
APP_CONTAINER: DurableObjectNamespace<MainAppContainerDO>;
// Optional: Pin requests to a specific instance
CONTAINER_INSTANCE_ID?: string;
}
const worker = {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
// Determine which container instance to use
// Priority: query param > env var > default
const instanceId =
url.searchParams.get('instance') ??
env.CONTAINER_INSTANCE_ID ??
DEFAULT_INSTANCE;
// Get container handle (creates if doesn't exist)
const container = getContainer(env.APP_CONTAINER, instanceId);
// ─────────────────────────────────────────────────────────────
// Health Check Endpoints - Return container state
// ─────────────────────────────────────────────────────────────
if (HEALTH_PATHS.has(url.pathname)) {
const state = await container.getState();
return Response.json({
status: state.status,
lastChange: new Date(state.lastChange).toISOString(),
exitCode: 'exitCode' in state ? state.exitCode : undefined
}, {
headers: { 'cache-control': 'no-store' }
});
}
// ─────────────────────────────────────────────────────────────
// Management Endpoints - Container lifecycle control
// ─────────────────────────────────────────────────────────────
// Force restart
if (url.pathname === '/_container/restart' && request.method === 'POST') {
await container.destroy();
return new Response('Restart requested', { status: 202 });
}
// Force start and wait for ready
if (url.pathname === '/_container/start' && request.method === 'POST') {
await container.startAndWaitForPorts({ ports: [CONTAINER_PORT] });
const state = await container.getState();
return Response.json({ message: 'Container started', state, instanceId });
}
// Get current status
if (url.pathname === '/_container/status') {
const state = await container.getState();
return Response.json({ state, instanceId });
}
// Graceful stop
if (url.pathname === '/_container/stop' && request.method === 'POST') {
await container.stop();
return new Response('Stop signal sent', { status: 202 });
}
// ─────────────────────────────────────────────────────────────
// Default: Proxy to container
// ─────────────────────────────────────────────────────────────
return container.fetch(request);
}
};
export default worker;

Worker package.json

{
"name": "containers-worker",
"private": true,
"type": "module",
"scripts": {
"dev": "wrangler dev",
"deploy": "wrangler deploy",
"deploy:production": "wrangler deploy --env production"
},
"dependencies": {
"@cloudflare/containers": "^0.0.30"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20241112.0",
"typescript": "^5.6.3",
"wrangler": "^3.88.0"
}
}

Wrangler Configuration (Annotated)

infra/cloudflare/containers-worker/wrangler.toml
# Worker name (appears in dashboard)
name = "main-app-worker"
# Entry point
main = "src/index.ts"
# Cloudflare API compatibility date
compatibility_date = "2024-11-16"
# Your Cloudflare account ID (find in dashboard URL or `wrangler whoami`)
account_id = "YOUR_ACCOUNT_ID"
# Enable workers.dev URL for testing
workers_dev = true
# ═══════════════════════════════════════════════════════════════════
# Environment Variables (non-secret)
# ═══════════════════════════════════════════════════════════════════
[vars]
CONTAINER_INSTANCE_ID = "main-app-dev"
# ═══════════════════════════════════════════════════════════════════
# Container Configuration
# ═══════════════════════════════════════════════════════════════════
[[containers]]
# Name for this container definition
name = "main-app"
# Must match the exported class name in your worker
class_name = "MainAppContainerDO"
# Full registry path: registry.cloudflare.com/<account_id>/<name>:<tag>
# This gets updated by `make cf-sync-wrangler-image`
image = "registry.cloudflare.com/YOUR_ACCOUNT_ID/main-app:v1.0.0"
# Max concurrent container instances
max_instances = 5
# ═══════════════════════════════════════════════════════════════════
# Durable Object Bindings
# ═══════════════════════════════════════════════════════════════════
[[durable_objects.bindings]]
# Name used in worker code: env.APP_CONTAINER
name = "APP_CONTAINER"
# Must match class_name in [[containers]]
class_name = "MainAppContainerDO"
# ═══════════════════════════════════════════════════════════════════
# Migrations (required for Durable Objects)
# ═══════════════════════════════════════════════════════════════════
[[migrations]]
tag = "v1"
new_sqlite_classes = ["MainAppContainerDO"]
# ═══════════════════════════════════════════════════════════════════
# Production Environment
# ═══════════════════════════════════════════════════════════════════
[env.production]
# Disable workers.dev URL in production
workers_dev = false
# Route configuration - attach to your domain
routes = [
{ pattern = "yourdomain.com/*", zone_name = "yourdomain.com" }
]
# Production container config (same structure, can have different settings)
[[env.production.containers]]
name = "main-app"
class_name = "MainAppContainerDO"
image = "registry.cloudflare.com/YOUR_ACCOUNT_ID/main-app:v1.0.0"
max_instances = 5
[[env.production.durable_objects.bindings]]
name = "APP_CONTAINER"
class_name = "MainAppContainerDO"
[env.production.vars]
CONTAINER_INSTANCE_ID = "main-app-prod"

Management Endpoints

Once deployed, these endpoints are available:

EndpointMethodDescription
/_healthGETContainer health status
/_container/statusGETDetailed container state
/_container/startPOSTForce start container
/_container/stopPOSTGraceful stop
/_container/restartPOSTDestroy and recreate

Usage:

Terminal window
# Check health
curl https://yourdomain.com/_health
# Get detailed status
curl https://yourdomain.com/_container/status
# Force start (useful for pre-warming)
curl -X POST https://yourdomain.com/_container/start
# Restart after deploy
curl -X POST https://yourdomain.com/_container/restart

Bootstrap: New Project Setup

1. Create the directory structure:

Terminal window
mkdir -p infra/cloudflare/containers-worker/src
mkdir -p infra/dockerfiles/main-app

2. Initialize the worker package:

Terminal window
cd infra/cloudflare/containers-worker
npm init -y
npm install @cloudflare/containers
npm install -D @cloudflare/workers-types typescript wrangler

3. Create tsconfig.json:

{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "bundler",
"lib": ["ES2022"],
"types": ["@cloudflare/workers-types"],
"strict": true,
"skipLibCheck": true
},
"include": ["src"]
}

4. Copy the worker code pattern (from above) to src/index.ts

5. Copy the wrangler.toml pattern and replace:

  • YOUR_ACCOUNT_ID with your Cloudflare account ID
  • main-app with your app name
  • yourdomain.com with your domain

6. Login and deploy:

Terminal window
npx wrangler login
npx wrangler deploy # deploys to workers.dev for testing

7. First container push:

Terminal window
# From project root
docker buildx build --platform linux/amd64 \
-f infra/dockerfiles/main-app/Dockerfile \
-t main-app:v1.0.0 \
--load \
infra/dockerfiles/main-app
npx wrangler containers push main-app:v1.0.0

8. Deploy to production:

Terminal window
npx wrangler deploy --env production

Troubleshooting

IssueSolution
Container not startingCheck /_container/status for exitCode
Image not foundRun wrangler containers images list to verify push
Wrong version deployedRun make cf-sync-wrangler-image then redeploy
Container sleepingHit any endpoint or POST to /_container/start
Auth errorsRun wrangler login again

Terraform Infrastructure

File Organization

infra/terraform/gpu-node/
├── main.tf # Resources (droplet, firewall)
├── variables.tf # Input variables with validation
├── outputs.tf # Computed outputs
├── versions.tf # Provider requirements
├── cloud-init.yaml.tpl # VM bootstrap template
├── env.example # Example environment file
├── terraform.tfvars.example # Example variables file
└── modules/ # Optional sub-modules
├── monitoring/
└── networking/

Makefile Module

make/gpu-infra.mk
TF_DIR := $(ROOT_DIR)/infra/terraform/gpu-node
SIDECAR_DIR := $(ROOT_DIR)/infra/dockerfiles/sidecar-service
# Load environment if exists
-include $(TF_DIR)/.env
# ─────────────────────────────────────────────────────────────────
# Terraform Commands
# ─────────────────────────────────────────────────────────────────
.PHONY: gpu-init
gpu-init: ## Initialize Terraform
@cd $(TF_DIR) && terraform init
.PHONY: gpu-validate
gpu-validate: ## Validate configuration
@cd $(TF_DIR) && terraform validate
.PHONY: gpu-plan
gpu-plan: ## Plan changes
@cd $(TF_DIR) && terraform plan -var-file=terraform.tfvars
.PHONY: gpu-apply
gpu-apply: ## Apply changes (creates resources)
@echo "⚠️ This will create billable resources!"
@cd $(TF_DIR) && terraform apply -auto-approve -var-file=terraform.tfvars
.PHONY: gpu-destroy
gpu-destroy: ## Destroy all resources
@cd $(TF_DIR) && terraform destroy -auto-approve -var-file=terraform.tfvars
.PHONY: gpu-output
gpu-output: ## Show outputs
@cd $(TF_DIR) && terraform output
# ─────────────────────────────────────────────────────────────────
# SSH Access
# ─────────────────────────────────────────────────────────────────
.PHONY: gpu-ssh
gpu-ssh: ## SSH into the instance
@IP=$$(cd $(TF_DIR) && terraform output -raw ipv4_address 2>/dev/null) && \
if [ -n "$$IP" ]; then ssh root@$$IP; \
else echo "❌ No instance running"; fi
.PHONY: gpu-logs
gpu-logs: ## View container logs
@IP=$$(cd $(TF_DIR) && terraform output -raw ipv4_address 2>/dev/null) && \
if [ -n "$$IP" ]; then \
ssh root@$$IP "docker compose logs -f --tail=100"; \
else echo "❌ No instance running"; fi
# ─────────────────────────────────────────────────────────────────
# Sidecar Docker
# ─────────────────────────────────────────────────────────────────
.PHONY: sidecar-build
sidecar-build: ## Build sidecar image
@cd $(SIDECAR_DIR) && docker buildx build --platform linux/amd64 -t sidecar:latest .
.PHONY: sidecar-push
sidecar-push: ## Push to GHCR
@cd $(SIDECAR_DIR) && docker buildx build --platform linux/amd64 \
-t ghcr.io/YOUR_ORG/sidecar:latest --push .
# ─────────────────────────────────────────────────────────────────
# Setup
# ─────────────────────────────────────────────────────────────────
.PHONY: gpu-setup
gpu-setup: ## Interactive setup wizard
@$(ROOT_DIR)/scripts/setup-gpu-infra.sh
.PHONY: gpu-test
gpu-test: ## Validate infrastructure
@$(ROOT_DIR)/scripts/validate-gpu-infra.sh

Example Files Pattern

infra/terraform/gpu-node/env.example
# Copy to .env and fill in values. NEVER commit .env!
DIGITALOCEAN_TOKEN=dop_v1_your_token_here
GHCR_TOKEN=ghp_your_github_token_here
infra/terraform/gpu-node/terraform.tfvars.example
# Copy to terraform.tfvars. NEVER commit terraform.tfvars!
session_id = "sess-example001"
user_id = "user-example"
auth_token = "generate-with-openssl-rand-hex-32"
region = "nyc2"
gpu_size = "gpu-h100x1-80gb"
admin_ssh_key_ids = []
ghcr_username = "your-github-username"
ghcr_token = "ghp_your_token"

Complete Bootstrap Checklist

1. Create Directory Structure

Terminal window
mkdir -p make infra/{dockerfiles,cloudflare,compose,terraform} scripts apps/main-app brain docs artifacts

2. Create Root Makefile

Copy the root Makefile pattern from Makefile Module System.

3. Create Initial Modules

At minimum:

  • make/dev.mk - Development server
  • make/release.mk - Versioning

4. Set Up Docker

  1. Create infra/dockerfiles/main-app/Dockerfile
  2. For sidecars: create self-contained folders with their own package.json

5. Set Up Release Tooling

Terminal window
cd apps/main-app
npm init -y # or yarn init
npm install -D standard-version

Add to package.json:

{
"scripts": {
"release": "standard-version",
"release:minor": "standard-version --release-as minor",
"release:major": "standard-version --release-as major",
"release:patch": "standard-version --release-as patch",
"release:dry": "standard-version --dry-run"
}
}

6. Create Version Scripts

  • scripts/app-version.mjs
  • scripts/release-sync-versions.mjs

7. Add to .gitignore

# Terraform
*.tfstate
*.tfstate.backup
*.tfvars
!*.tfvars.example
.terraform/
# Environment
**/.env
!**/.env.example
# Artifacts
artifacts/
dist/
node_modules/

8. Verify Setup

Terminal window
make help # Should list all targets
make release-dry # Should preview release

Quick Reference

CommandDescription
make helpList all targets
make devStart dev server
make releaseCreate release (auto-bump)
make release-patchForce patch bump
make release-minorForce minor bump
make release-majorForce major bump
make release-dryPreview release
make cf-deployFull Cloudflare deploy
make cf-container-build-localBuild Docker image
make gpu-applyCreate GPU infrastructure
make gpu-destroyTear down infrastructure
make gpu-sshSSH into instance
make make-keyGenerate secret
make make-key FORMAT=base64Generate base64 secret