Skip to content
Draft
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
13 changes: 8 additions & 5 deletions docker/swarm/stacks/api/.env.sample
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
# -- Docker Configuration
DOCKER_REGISTRY=
DEPLOYMENT_ENV=
DEPLOYMENT_VERSION=
DEPLOYMENT_TLD=

# -- Runtime
NODE_ENV=production
FREECODECAMP_NODE_ENV=production
# NODE_ENV=production
# FREECODECAMP_NODE_ENV=production
# PORT=3000
# HOST=0.0.0.0

Expand All @@ -13,6 +15,7 @@ MONGOHQ_URL=

# -- Logging
# FCC_API_LOG_LEVEL=info
LOKI_URL=
SENTRY_DSN=
SENTRY_ENVIRONMENT=

Expand All @@ -28,11 +31,11 @@ AUTH0_DOMAIN=
# -- Session, Cookie and JWT encryption strings
JWT_SECRET=
COOKIE_SECRET=
COOKIE_DOMAIN=.freecodecamp.org
# COOKIE_DOMAIN=.freecodecamp.org

# -- Email
EMAIL_PROVIDER=ses
SES_REGION=us-east-1
# EMAIL_PROVIDER=ses
# SES_REGION=us-east-1
SES_ID=
SES_SECRET=

Expand Down
68 changes: 68 additions & 0 deletions docker/swarm/stacks/api/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# freeCodeCamp API Stack Deployment Makefile

.PHONY: help validate decrypt config debug deploy clean

# Default target
help:
@echo "freeCodeCamp API Stack Deployment"
@echo ""
@echo "Available targets:"
@echo " validate Validate all required environment variables"
@echo " decrypt Decrypt age-encrypted secrets (requires AGE_* vars)"
@echo " config Validate Docker stack configuration"
@echo " debug Save stack configuration to debug file"
@echo " deploy Deploy stack (auto-detects dev/prod from DEPLOYMENT_TLD)"
@echo " clean Remove temporary files"
@echo ""
@echo "Prerequisites:"
@echo " - Set DEPLOYMENT_VERSION and DEPLOYMENT_TLD environment variables"
@echo " - For encrypted secrets: run 'make decrypt' first"
@echo " - For manual deployment: source variables from .env or export manually"

# Validate environment variables
validate:
@echo "Validating environment variables..."
@./scripts/validate-env.sh

# Decrypt age-encrypted secrets
decrypt:
@echo "Decrypting secrets..."
@./scripts/decrypt-secrets.sh --save-env

# Validate Docker stack configuration
config:
@echo "Validating Docker stack configuration..."
@docker stack config -c stack-api.yml > /dev/null
@echo "Stack configuration is valid"

# Save debug configuration
debug:
@echo "Generating debug configuration..."
@if [ -z "$(DEPLOYMENT_VERSION)" ]; then \
echo "ERROR: DEPLOYMENT_VERSION is required"; \
exit 1; \
fi
@docker stack config -c stack-api.yml > debug-docker-stack-config-$(DEPLOYMENT_VERSION).yml
@echo "Debug configuration saved to debug-docker-stack-config-$(DEPLOYMENT_VERSION).yml"

# Deploy stack (auto-detects environment from DEPLOYMENT_TLD)
deploy: validate config
@echo "Deploying API stack..."
@if [ "$(DEPLOYMENT_TLD)" = "dev" ]; then \
echo "Deploying to staging environment..."; \
docker stack deploy -c stack-api.yml --prune --with-registry-auth --detach=false stg-api; \
echo "Successfully deployed stg-api stack"; \
elif [ "$(DEPLOYMENT_TLD)" = "org" ]; then \
echo "Deploying to production environment..."; \
docker stack deploy -c stack-api.yml --prune --with-registry-auth --detach=false prd-api; \
echo "Successfully deployed prd-api stack"; \
else \
echo "ERROR: DEPLOYMENT_TLD must be 'dev' or 'org'"; \
exit 1; \
fi

# Clean up temporary files
clean:
@echo "Cleaning up temporary files..."
@rm -f .env debug-docker-stack-config-*.yml
@echo "Cleanup complete"
98 changes: 94 additions & 4 deletions docker/swarm/stacks/api/README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,97 @@
## Usage
# API Stack

This stack defines all the services for the API. Name the stacks as per the environment ex: `prd-api`, etc. Set up the env values from the `.env.sample` file within the Portainer UI.
Docker Swarm stack configuration for freeCodeCamp Learn API.

**Caddyfile**
## Services

The Caddyfile is used to proxy the API to the correct port. It is located in the [`Caddyfile`](./Caddyfile) file. You will need to create a new Docker config for each new version of the file. Check the stack file for the correct name, and create the config within the `configs` section of Portainer. You can then set the `CADDY_CONFIG_NAME` environment variable to the name of the config you created.
| Service | Port | Health Check |
| ----------------- | --------- | -------------- |
| **svc-api-alpha** | 2345:3000 | `/status/ping` |
| **svc-api-bravo** | 2346:3000 | `/status/ping` |

## Quick Start

### 1. Prerequisites

- Docker Swarm cluster initialized
- Nodes labeled with `api.enabled=true` and `api.variant=${DEPLOYMENT_TLD}`

### 2. Deploy with Encrypted Secrets

```bash
# Set required variables
export AGE_ENCRYPTED_ASC_SECRETS="<encrypted-secrets>"
export AGE_SECRET_KEY="<decryption-key>"
export DEPLOYMENT_VERSION="<version>"
export DEPLOYMENT_TLD="dev" # or "org" for production

# Deploy in 2 steps
make decrypt # Decrypt secrets to .env file
source .env # Source environment variables
make deploy # Deploy stack (auto-detects staging/production)
```

### 3. Deploy with Manual Variables

```bash
# Copy template and set values
cp .env.sample .env
# Edit .env with actual values...

source .env # Source environment variables
make deploy # Deploy stack
```

## Available Commands

- `make help` - Show all available commands
- `make decrypt` - Decrypt age-encrypted secrets to `.env` file
- `make validate` - Validate all required environment variables
- `make config` - Validate Docker stack configuration
- `make deploy` - Deploy stack (auto-detects dev/prod from DEPLOYMENT_TLD)
- `make debug` - Generate debug configuration file
- `make clean` - Remove temporary files

## Environment Variables

See `.env.sample` for the complete and current list.

## Manual Deployment (Advanced)

If you need to deploy without the Makefile:

### 1. Label Nodes

```bash
docker node update --label-add "api.enabled=true" <node-id>
docker node update --label-add "api.variant=dev" <node-id> # or "org" for production
```

### 2. Validate and Deploy

```bash
# Validate environment
./scripts/validate-env.sh

# Validate configuration
docker stack config -c stack-api.yml > /dev/null

# Deploy stack manually
docker stack deploy -c stack-api.yml --prune --with-registry-auth --detach=false <stack-name>
```

**Stack naming convention:**

- `DEPLOYMENT_TLD=dev` → `stg-api`
- `DEPLOYMENT_TLD=org` → `prd-api`

## Notes

- Use `make help` to see all available deployment commands
- Use `.env.sample` as template for required variables
- Scripts validate environment and handle age decryption automatically
- Docker Swarm doesn't support env files - source variables before deployment
- Services use host networking mode with placement constraints
- Health checks via `/status/ping?checker=swarm-manager`
- Loki logging with structured JSON pipeline
- Rolling updates with automatic rollback on 30% failure threshold
89 changes: 89 additions & 0 deletions docker/swarm/stacks/api/scripts/decrypt-secrets.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
#!/bin/bash
set -e

# API Stack Age Secret Decryption Script
# Decrypts age-encrypted secrets and sources environment variables

if [[ -z "$AGE_ENCRYPTED_ASC_SECRETS" || -z "$AGE_SECRET_KEY" ]]; then
echo "ERROR: AGE_ENCRYPTED_ASC_SECRETS and AGE_SECRET_KEY environment variables are required"
exit 1
fi

echo "Decrypting secrets using age..."

# Check if age is installed
if ! command -v age &> /dev/null; then
echo "ERROR: age is not installed. Install with: brew install age (macOS) or apt-get install age (Ubuntu)"
exit 1
fi

# Create temporary files
SECRETS_FILE=$(mktemp)
AGE_KEY_FILE=$(mktemp)
ENV_FILE=$(mktemp)
ENV_TMP_FILE=$(mktemp)

# Cleanup function
cleanup() {
rm -f "$SECRETS_FILE" "$AGE_KEY_FILE" "$ENV_FILE" "$ENV_TMP_FILE"
}
trap cleanup EXIT

echo "Creating temporary files..."

# Write encrypted secrets and key to temporary files
echo "$AGE_ENCRYPTED_ASC_SECRETS" > "$SECRETS_FILE"
echo "$AGE_SECRET_KEY" > "$AGE_KEY_FILE"
chmod 600 "$AGE_KEY_FILE"

echo "Decrypting secrets..."

# Decrypt secrets
if ! age --identity "$AGE_KEY_FILE" --decrypt "$SECRETS_FILE" > "$ENV_FILE"; then
echo "ERROR: Failed to decrypt secrets"
exit 1
fi

echo "Cleaning up duplicate environment variables..."

# Clean duplicates from .env (keep last occurrence of each variable)
touch "$ENV_TMP_FILE"
while IFS= read -r line; do
if [[ $line =~ ^[A-Za-z0-9_]+=.*$ ]]; then
# Extract the key (part before the first =)
key=${line%%=*}
# Remove any previous line with this key
sed -i.bak "/^${key}=/d" "$ENV_TMP_FILE" && rm -f "${ENV_TMP_FILE}.bak"
fi
# Append the current line
echo "$line" >> "$ENV_TMP_FILE"
done < "$ENV_FILE"

echo "Adding deployment variables..."

# Add deployment variables if they exist
{
[[ -n "$DEPLOYMENT_VERSION" ]] && echo "DEPLOYMENT_VERSION=$DEPLOYMENT_VERSION"
[[ -n "$DEPLOYMENT_TLD" ]] && echo "DEPLOYMENT_TLD=$DEPLOYMENT_TLD"
[[ -n "$DEPLOYMENT_ENV" ]] && echo "DEPLOYMENT_ENV=$DEPLOYMENT_ENV"
[[ -n "$FCC_API_LOG_LEVEL" ]] && echo "FCC_API_LOG_LEVEL=$FCC_API_LOG_LEVEL"
} >> "$ENV_TMP_FILE"

echo "Sourcing environment variables..."

# Source all variables from the cleaned file
while IFS='=' read -r key value; do
if [[ -n "$key" && ! "$key" =~ ^# ]]; then
export "${key}=${value}"
echo " $key"
fi
done < "$ENV_TMP_FILE"

VAR_COUNT=$(grep -c '^[A-Za-z0-9_]=' "$ENV_TMP_FILE" || echo "0")
echo "Successfully decrypted and sourced $VAR_COUNT environment variables"

# Optional: Save to .env file for manual inspection
if [[ "$1" == "--save-env" ]]; then
cp "$ENV_TMP_FILE" .env
echo "Environment variables saved to .env file"
fi
61 changes: 61 additions & 0 deletions docker/swarm/stacks/api/scripts/validate-env.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
#!/bin/bash
set -e

# API Stack Environment Validation Script
# Validates all required environment variables are set

REQUIRED_VARS=(
"DOCKER_REGISTRY"
"MONGOHQ_URL"
"SENTRY_DSN"
"SENTRY_ENVIRONMENT"
"AUTH0_CLIENT_ID"
"AUTH0_CLIENT_SECRET"
"AUTH0_DOMAIN"
"JWT_SECRET"
"COOKIE_SECRET"
"COOKIE_DOMAIN"
"SES_ID"
"SES_SECRET"
"GROWTHBOOK_FASTIFY_API_HOST"
"GROWTHBOOK_FASTIFY_CLIENT_KEY"
"HOME_LOCATION"
"API_LOCATION"
"STRIPE_SECRET_KEY"
"LOKI_URL"
"DEPLOYMENT_VERSION"
"DEPLOYMENT_TLD"
"DEPLOYMENT_ENV"
"FCC_API_LOG_LEVEL"
)

echo "Validating environment variables for API stack deployment..."

MISSING_VARS=()
for var in "${REQUIRED_VARS[@]}"; do
if [[ -z "${!var}" ]]; then
MISSING_VARS+=("$var")
fi
done

if [[ ${#MISSING_VARS[@]} -gt 0 ]]; then
echo "ERROR: The following required environment variables are missing or empty:"
printf ' - %s\n' "${MISSING_VARS[@]}"
echo ""
echo "Use .env.sample as a reference for all required variables"
exit 1
fi

echo "All required environment variables are set (${#REQUIRED_VARS[@]} variables checked)"

# Optional: Validate version format
if [[ ! "$DEPLOYMENT_VERSION" =~ ^[a-zA-Z0-9._-]+$ ]]; then
echo "WARNING: DEPLOYMENT_VERSION format may be invalid: $DEPLOYMENT_VERSION"
fi

# Optional: Validate TLD
if [[ "$DEPLOYMENT_TLD" != "dev" && "$DEPLOYMENT_TLD" != "org" ]]; then
echo "WARNING: DEPLOYMENT_TLD should be 'dev' or 'org', got: $DEPLOYMENT_TLD"
fi

echo "Environment validation passed - ready for deployment"