DeployStack Docs

Backend Environment Variables

The DeployStack backend uses Node.js environment variables that work seamlessly across development and production environments. This system supports both local .env files for development and Docker environment variable injection for production deployments.

Overview

The backend environment system consists of two main approaches:

  1. Development Environment: Uses .env files loaded via Node.js --env-file flag with nodemon
  2. Production Environment: Uses Docker environment variables injected at container runtime

This approach ensures that:

  • Developers can work with standard .env files during development using npm run dev
  • Production deployments can inject variables at runtime without rebuilding the application
  • Environment variables are directly accessible via process.env throughout the Node.js application
  • The same codebase works seamlessly in both environments

Environment Variable Types

Development Environment Variables

During development, the backend loads environment variables from .env files using Node.js's built-in --env-file support:

# .env
PORT=3000
NODE_ENV=development
DEPLOYSTACK_FRONTEND_URL=http://localhost:5173
COOKIE_SECRET=your-secure-cookie-secret-here

Production Environment Variables

In production Docker containers, variables are injected at runtime via Docker's environment variable system:

# Docker run example
docker run -e PORT=3000 \
           -e NODE_ENV=production \
           -e DEPLOYSTACK_FRONTEND_URL="https://app.deploystack.com" \
           -e COOKIE_SECRET="your-production-cookie-secret" \
           deploystack/backend:latest

Development Setup

Environment Files

Create environment files in the services/backend directory:

.env (Base Configuration)

# Server Configuration
PORT=3000
NODE_ENV=development
HOST=localhost

# Frontend Integration
DEPLOYSTACK_FRONTEND_URL=http://localhost:5173

# Security
COOKIE_SECRET=a-very-secret-and-strong-secret-for-cookies
DEPLOYSTACK_ENCRYPTION_SECRET=your-encryption-secret-here

# Logging
LOG_LEVEL=debug

# OAuth Configuration (optional for development)
GITHUB_CLIENT_ID=your-github-client-id
GITHUB_CLIENT_SECRET=your-github-client-secret
GITHUB_REDIRECT_URI=http://localhost:3000/api/auth/github/callback

# Application
DEPLOYSTACK_BACKEND_VERSION=0.20.5

.env.local (Local Overrides)

# Local development environment variables
# This file is gitignored and used for local customization
PORT=3001
DEPLOYSTACK_FRONTEND_URL=http://localhost:5174
LOG_LEVEL=info

Environment File Priority

Node.js with --env-file loads environment variables in this order:

  1. System environment variables (highest priority)
  2. .env file variables
  3. Default values in code (lowest priority)

Note: Unlike some frameworks, Node.js --env-file doesn't support multiple env files by default. Use .env.local by renaming it to .env for local development.

Development Example

# Navigate to backend directory
cd services/backend

# Create your local environment file
cp .env .env.local

# Edit .env.local with your local settings
echo "PORT=3001" >> .env.local
echo "LOG_LEVEL=info" >> .env.local

# Rename for development (or modify .env directly)
mv .env.local .env

# Start development server
npm run dev

Nodemon Configuration

The development server uses nodemon with the following configuration (from nodemon.json):

{
  "watch": ["src"],
  "ext": "ts,js,json",
  "ignore": ["src/**/*.test.ts", "src/**/*.spec.ts", "tests/**/*"],
  "exec": "node --env-file=.env --require ts-node/register src/index.ts",
  "env": {
    "NODE_ENV": "development"
  },
  "delay": 1000
}

This configuration:

  • Loads environment variables from .env using --env-file=.env
  • Sets NODE_ENV=development by default
  • Watches TypeScript files for changes
  • Uses ts-node for TypeScript compilation

Production Deployment

Docker Environment Variables

The production Docker container accepts environment variables at runtime:

# Production deployment
docker run -d -p 3000:3000 \
  -e NODE_ENV=production \
  -e PORT=3000 \
  -e DEPLOYSTACK_FRONTEND_URL="https://app.deploystack.com" \
  -e COOKIE_SECRET="your-production-secret" \
  -e DEPLOYSTACK_ENCRYPTION_SECRET="your-encryption-secret" \
  deploystack/backend:latest

Docker Compose Example

version: '3.8'
services:
  backend:
    image: deploystack/backend:latest
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
      - PORT=3000
      - DEPLOYSTACK_FRONTEND_URL=https://app.deploystack.com
      - COOKIE_SECRET=your-production-cookie-secret
      - DEPLOYSTACK_ENCRYPTION_SECRET=your-encryption-secret
      - GITHUB_CLIENT_ID=your-github-client-id
      - GITHUB_CLIENT_SECRET=your-github-client-secret
      - GITHUB_REDIRECT_URI=https://api.deploystack.com/api/auth/github/callback
    volumes:
      - ./data:/app/data  # For SQLite database persistence

Dockerfile Environment Handling

The production Dockerfile creates a default .env file with basic settings:

# Create a default .env file
RUN echo "NODE_ENV=production" > .env && \
    echo "PORT=3000" >> .env && \
    echo "DEPLOYSTACK_BACKEND_VERSION=${DEPLOYSTACK_BACKEND_VERSION:-$(node -e "console.log(require('./package.json').version)")}" >> .env

# Start with env file
CMD ["node", "--env-file=.env", "dist/index.js"]

Runtime environment variables will override these defaults.

Using Environment Variables in Code

Accessing Variables

Environment variables are directly accessible via process.env in Node.js:

// Direct access
const port = process.env.PORT || '3000'
const nodeEnv = process.env.NODE_ENV || 'development'
const frontendUrl = process.env.DEPLOYSTACK_FRONTEND_URL

// Type-safe access with validation
function getRequiredEnv(key: string): string {
  const value = process.env[key]
  if (!value) {
    throw new Error(`Required environment variable ${key} is not set`)
  }
  return value
}

const cookieSecret = getRequiredEnv('COOKIE_SECRET')

Server Configuration Example

// src/server.ts
import fastify from 'fastify'

export const createServer = async () => {
  const server = fastify({
    logger: {
      level: process.env.LOG_LEVEL || 'info'
    }
  })

  // Cookie configuration
  await server.register(fastifyCookie, {
    secret: process.env.COOKIE_SECRET || 'fallback-secret',
    parseOptions: {}
  })

  // CORS configuration
  const frontendUrl = process.env.DEPLOYSTACK_FRONTEND_URL
  if (frontendUrl) {
    await server.register(fastifyCors, {
      origin: frontendUrl,
      credentials: true
    })
  }

  return server
}

Database Configuration Example

// src/db/setup.ts
const setupDatabase = () => {
  const isTestEnv = process.env.NODE_ENV === 'test'
  const sqliteDbFileName = isTestEnv ? 'deploystack.test.db' : 'deploystack.db'
  
  const dbPath = path.join(process.cwd(), 'data', sqliteDbFileName)
  
  return new Database(dbPath)
}

Plugin System Configuration

// src/server.ts
const isDevelopment = process.env.NODE_ENV !== 'production'
const pluginManager = new PluginManager({
  paths: [
    process.env.PLUGINS_PATH || (isDevelopment 
      ? path.join(process.cwd(), 'src', 'plugins')
      : path.join(__dirname, 'plugins')),
  ],
  plugins: {}
})

Adding New Environment Variables

Step 1: Add to Environment Files

Add the new variable to your .env file:

# .env
PORT=3000
NODE_ENV=development
DEPLOYSTACK_FRONTEND_URL=http://localhost:5173
NEW_FEATURE_ENABLED=true
NEW_API_ENDPOINT=https://api.example.com

Step 2: Use in Code

Access the variable in your TypeScript code:

// src/config/features.ts
export const isNewFeatureEnabled = (): boolean => {
  const value = process.env.NEW_FEATURE_ENABLED
  return value === 'true' || value === '1'
}

export const getApiEndpoint = (): string => {
  return process.env.NEW_API_ENDPOINT || 'https://api.default.com'
}

Step 3: Update Production Configuration

Add the variable to your production deployment configuration:

# Docker
docker run -e NEW_FEATURE_ENABLED=true \
           -e NEW_API_ENDPOINT=https://prod-api.example.com \
           deploystack/backend:latest
# Docker Compose
environment:
  - NEW_FEATURE_ENABLED=true
  - NEW_API_ENDPOINT=https://prod-api.example.com

Step 4: Create Type-Safe Helpers (Optional)

For better type safety and validation:

// src/utils/env.ts
interface EnvironmentConfig {
  port: number
  nodeEnv: string
  frontendUrl: string
  newFeatureEnabled: boolean
  newApiEndpoint: string
}

export function getEnvironmentConfig(): EnvironmentConfig {
  return {
    port: parseInt(process.env.PORT || '3000', 10),
    nodeEnv: process.env.NODE_ENV || 'development',
    frontendUrl: process.env.DEPLOYSTACK_FRONTEND_URL || 'http://localhost:5173',
    newFeatureEnabled: process.env.NEW_FEATURE_ENABLED === 'true',
    newApiEndpoint: process.env.NEW_API_ENDPOINT || 'https://api.default.com'
  }
}

// Usage
import { getEnvironmentConfig } from '@/utils/env'

const config = getEnvironmentConfig()
if (config.newFeatureEnabled) {
  // Use new feature
}

Environment Variable Naming Conventions

Backend Environment Variables

  • Use SCREAMING_SNAKE_CASE format
  • Be descriptive and specific
  • Group related variables with prefixes
 Good
PORT=3000
NODE_ENV=development
DEPLOYSTACK_FRONTEND_URL=http://localhost:5173
DEPLOYSTACK_ENCRYPTION_SECRET=secret
GITHUB_CLIENT_ID=client-id
GITHUB_CLIENT_SECRET=client-secret

 Bad
port=3000                    # Wrong case
URL=http://localhost:5173    # Not descriptive
SECRET=secret               # Too generic

Common Patterns

# Server Configuration
PORT=3000
HOST=localhost
NODE_ENV=development

# Application Specific (use DEPLOYSTACK_ prefix)
DEPLOYSTACK_FRONTEND_URL=http://localhost:5173
DEPLOYSTACK_BACKEND_VERSION=0.20.5
DEPLOYSTACK_ENCRYPTION_SECRET=secret

# Third-party Services (use service name prefix)
GITHUB_CLIENT_ID=client-id
GITHUB_CLIENT_SECRET=client-secret
SMTP_HOST=smtp.example.com
SMTP_PORT=587

# Feature Flags
ENABLE_SWAGGER_DOCS=true
ENABLE_DEBUG_MODE=false

Debugging Environment Variables

Development Debugging

// Add to your server startup
console.log('Environment Variables:')
console.log('PORT:', process.env.PORT)
console.log('NODE_ENV:', process.env.NODE_ENV)
console.log('FRONTEND_URL:', process.env.DEPLOYSTACK_FRONTEND_URL)

// Or create a debug endpoint (development only)
if (process.env.NODE_ENV === 'development') {
  server.get('/debug/env', async (request, reply) => {
    const safeEnvVars = {
      PORT: process.env.PORT,
      NODE_ENV: process.env.NODE_ENV,
      DEPLOYSTACK_FRONTEND_URL: process.env.DEPLOYSTACK_FRONTEND_URL,
      LOG_LEVEL: process.env.LOG_LEVEL,
      // Don't expose secrets!
    }
    return safeEnvVars
  })
}

Production Debugging

# Check environment variables in Docker container
docker exec -it container-name env | grep DEPLOYSTACK

# Or check specific variables
docker exec -it container-name printenv PORT
docker exec -it container-name printenv NODE_ENV

Startup Banner

The backend displays a startup banner with key environment information:

// src/utils/banner.ts
export const displayStartupBanner = (port: number): void => {
  const version = process.env.DEPLOYSTACK_BACKEND_VERSION || '0.1.0'
  const environment = process.env.NODE_ENV || 'development'
  
  console.log(`
  ╔══════════════════════════════════════════════════════════════════════════════╗
  ║                            🚀 DeployStack Backend                            ║
  ║                                                                              ║
  ║         Running on port ${port}                                              ║
  ║         Environment: ${environment}                                          ║
  ║         Version: ${version}                                                  ║
  ╚══════════════════════════════════════════════════════════════════════════════╝
  `)
}