Environment Variables

How Rakiba handles environment variables across development, staging, and production. Includes .env files, Aero configuration, Docker, and VPS deployment.

Overview

Rakiba uses environment variables for secrets and runtime configuration. The system is designed to be consistent across all environments while keeping secrets out of version control.

EnvironmentHow Env Vars Are Loaded
Developmentbb clj parses .env and passes to JVM
Testingbb test parses .env same as development
Local SandboxDocker container with .env uploaded via bb deploy
VPS ProductionSystemd EnvironmentFile directive

File Structure

project/
├── .env                    # Your secrets (gitignored)
├── .env.local              # Local overrides (gitignored)
├── .env.example            # Template showing required vars (committed)
├── .env.encrypted          # Encrypted secrets (committed, optional)
└── resources/
    └── config.edn          # Aero config referencing env vars (committed)

What Gets Committed

FileCommitted?Purpose
.envNOActive secrets for local dev
.env.localNOLocal overrides
.env.*.localNOEnvironment-specific local overrides
.env.exampleYESTemplate with empty/placeholder values
.env.encryptedYESEncrypted secrets (safe to commit)
resources/config.ednYESConfiguration with #env references

The .env File

Format

# .env - DO NOT COMMIT

# Database
DATABASE_URL=postgresql://user:pass@localhost:5432/myapp_dev

# AI Providers
ANTHROPIC_API_KEY=sk-ant-api03-xxxxx

# Object Storage
S3_ACCESS_KEY_ID=GKxxxxx
S3_SECRET_ACCESS_KEY=xxxxx
S3_ENDPOINT=http://localhost:3900

# Cache
VALKEY_HOST=localhost
VALKEY_PORT=6379

Syntax rules:

  • No spaces around =
  • No quotes needed (even for values with spaces)
  • No export prefix
  • Comments start with #

.env.example Template

Always commit a template showing required variables:

# .env.example - Copy to .env and fill in values

# Required
DATABASE_URL=postgresql://user:pass@localhost:5432/myapp_dev
ANTHROPIC_API_KEY=

# Optional (have defaults in config.edn)
PORT=3060
LOG_LEVEL=info
S3_ENDPOINT=http://localhost:3900

Configuration with Aero

Environment variables are referenced in resources/config.edn using Aero reader tags.

Basic Pattern

{;; Profile from PROFILE env var (defaults to :dev)
 :env/profile #profile {:dev :dev :test :test :prod :prod}

 ;; Server - PORT env var with default 3060
 :server {:port #long #or [#env PORT 3060]
          :host #or [#env HTTP_HOST "0.0.0.0"]}

 ;; nREPL for development
 :nrepl {:port #long #or [#env NREPL_PORT 7060]}

 ;; Database - required env var (no default)
 :database {:url #env DATABASE_URL}

 ;; Storage - mix of required and optional
 :storage {:s3 {:endpoint #or [#env S3_ENDPOINT "http://localhost:3900"]
                :access-key-id #env S3_ACCESS_KEY_ID
                :secret-access-key #env S3_SECRET_ACCESS_KEY
                :bucket #or [#env S3_BUCKET "myapp"]}}}

Aero Reader Tags

TagPurposeExample
#envRead env var (nil if missing)#env DATABASE_URL
#orFallback chain#or [#env PORT 3000]
#profilePer-environment values#profile {:dev "localhost" :prod #env DB_HOST}
#longParse as integer#long #or [#env PORT 3000]
#keywordParse as keyword#keyword #env LOG_LEVEL

Required vs Optional

;; Required - nil if not set, app should fail fast
:api-key #env ANTHROPIC_API_KEY

;; Optional - has sensible default
:port #long #or [#env PORT 3060]

;; Profile-specific default
:host #profile {:dev "localhost" :prod #env DB_HOST}

Development

Running with Environment Variables

# Standard development (loads .env automatically)
bb clj

# Override specific variables
PORT=8080 bb clj

# Use specific profile
PROFILE=prod bb clj

# Multiple overrides
PORT=8080 LOG_LEVEL=debug DATABASE_URL=postgresql://... bb clj

How bb clj Loads .env

When you run bb clj, it:

  1. Parses .env file (KEY=value format)
  2. Passes variables to JVM via {:extra-env env-vars}
  3. Aero reads them via #env tags when loading config.edn
;; Simplified bb clj implementation
(defn- parse-env-file [path]
  (when (fs/exists? path)
    (into {}
      (for [line (str/split-lines (slurp path))
            :let [[_ k v] (re-matches #"^([A-Za-z_][A-Za-z0-9_]*)=(.*)$" line)]
            :when k]
        [k v]))))

(defn bb-clj []
  (let [env-vars (parse-env-file ".env")]
    (shell {:extra-env env-vars} "clojure" "-M:dev:server")))

Accessing Config in Code

(ns myapp.core
  (:require [rakiba.lib.env :as env]))

;; Optional with default
(env/get :port 3000)
(env/get-int :port 3000)
(env/get-keyword :log-level :info)

;; Required (throws if missing)
(env/req :database-url)
(env/req-int :port)

;; Direct config access
(get-in env/config [:storage :s3 :endpoint])

Local Sandbox (Docker)

Local sandboxes simulate production using Docker containers.

Setup

# Create sandbox
bb local:create ubuntu@24.04 --name dev

# Deploy project (uploads .env to container)
bb deploy my-app --target dev

# Access
open http://my-app.rakiba.test:9080

How It Works

  1. bb deploy reads your local .env file
  2. Uploads it to /home/rakiba/<project>/.env inside the container
  3. Creates systemd service with EnvironmentFile directive
  4. Service loads env vars at startup

Container Environment

The sandbox provisions a full stack with these services:

ServiceDefault PortEnv Vars
PostgreSQL5432Credentials in .local-envs/
Valkey6379ACL credentials
Garage (S3)3900Access keys
Datomic4334Connection URI
nginx9080N/A

Credentials for these services are stored in .local-envs/<sandbox-name>.edn and merged into your project’s config.

VPS Production

Adding a VPS Target

bb vps:add root@192.168.1.100 --name prod

Deploy

bb deploy my-app --target prod

What Happens

  1. Build: Creates uberjar with bb build
  2. Upload: Transfers JAR and .env via rsync
  3. Systemd: Creates/updates service file
  4. Start: Starts service with blue/green deployment

Systemd Service Template

The deployment creates /etc/systemd/system/rakiba-<project>.service:

[Unit]
Description=my-app Rakiba Application
After=network.target

[Service]
Type=simple
User=rakiba
Group=rakiba
WorkingDirectory=/home/rakiba/my-app

# Load secrets from .env file
EnvironmentFile=-/home/rakiba/my-app/.env

# Hardcoded values (override .env)
Environment=PROFILE=prod
Environment=PORT=8001
Environment=NREPL_PORT=7001

ExecStart=/usr/bin/java -jar current.jar
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

Key points:

  • EnvironmentFile=- loads .env (the - means no error if file missing)
  • Environment= sets explicit values that override .env
  • Both sources combine at runtime

File Permissions

The .env file on the server has restricted permissions:

# Set by bb deploy
chmod 600 /home/rakiba/my-app/.env
chown rakiba:rakiba /home/rakiba/my-app/.env

Checking Production Env Vars

# SSH to server
ssh user@server

# Check .env file
cat /home/rakiba/my-app/.env

# Check what systemd loaded
systemctl show rakiba-my-app --property=Environment

# View service logs
journalctl -u rakiba-my-app -f

Variable Precedence

From highest to lowest priority:

  1. Shell environment - PORT=8080 bb clj
  2. EnvironmentFile (.env) - Loaded by bb clj or systemd
  3. Systemd Environment= - Hardcoded in service file
  4. Aero #env - Reads from combined env
  5. Aero #profile - Profile-specific defaults
  6. Aero #or default - Static fallback value

Example

;; config.edn
:port #long #or [#env PORT #profile {:dev 3060 :prod 8080}]
ScenarioPORT EnvPROFILEResult
bb cljnot setdev3060
PORT=9000 bb clj9000dev9000
PROFILE=prod bb cljnot setprod8080
VPS (systemd sets PORT=8001)8001prod8001

Encryption

For team environments, encrypt .env and commit it safely.

Encrypt

# Uses your SSH ed25519 public key
bb env:encrypt

# Creates .env.encrypted (safe to commit)
git add .env.encrypted
git commit -m "Update encrypted secrets"

Decrypt

# Uses your SSH private key
bb env:decrypt

# Creates .env (gitignored)

How It Works

Uses age encryption with SSH keys:

# Encrypt (what bb env:encrypt does)
age -R ~/.ssh/id_ed25519.pub -o .env.encrypted .env

# Decrypt (what bb env:decrypt does)
age -d -i ~/.ssh/id_ed25519 -o .env .env.encrypted

Team Setup

  1. Collect team members’ public keys
  2. Encrypt with multiple recipients:
    age -R alice.pub -R bob.pub -o .env.encrypted .env
  3. Any recipient can decrypt with their private key

Environment-Specific Patterns

Development

# .env (local development)
PROFILE=dev
PORT=3060
DATABASE_URL=postgresql://localhost:5432/myapp_dev
ANTHROPIC_API_KEY=sk-ant-...
LOG_LEVEL=debug

Staging (Local Sandbox)

# .env (same file, different DATABASE_URL)
PROFILE=staging
PORT=3060
DATABASE_URL=postgresql://localhost:5432/myapp_staging
ANTHROPIC_API_KEY=sk-ant-...  # Can use same key or staging-specific
LOG_LEVEL=info

Production (VPS)

# .env (uploaded to server)
PROFILE=prod
# PORT set by systemd, not in .env
DATABASE_URL=postgresql://db.internal:5432/myapp_prod
ANTHROPIC_API_KEY=sk-ant-...  # Production API key
LOG_LEVEL=warn

Common Variables Reference

VariableRequiredDefaultDescription
PROFILENodevActive profile (dev, test, prod)
PORTNo3060HTTP server port
HTTP_HOSTNo0.0.0.0HTTP bind address
NREPL_PORTNo7060nREPL port (dev only)
LOG_LEVELNoinfoLogging level
DATABASE_URLDepends-PostgreSQL connection string
DATOMIC_URIDepends-Datomic connection URI
S3_ENDPOINTDepends-S3/Garage endpoint
S3_ACCESS_KEY_IDDepends-S3 access key
S3_SECRET_ACCESS_KEYDepends-S3 secret key
S3_BUCKETNoproject nameDefault bucket
VALKEY_HOSTNolocalhostValkey/Redis host
VALKEY_PORTNo6379Valkey/Redis port
ANTHROPIC_API_KEYDepends-Claude API key

Troubleshooting

Variable Not Being Read

# Check .env syntax
cat .env | grep MY_VAR

# Check shell environment
echo $MY_VAR

# Check in REPL
(System/getenv "MY_VAR")

Config Returns nil

;; Check if config loaded
env/config  ;; If {} or nil, config.edn not found

;; Verify file location
;; Must be at resources/config.edn, not project root

Type Errors (String Instead of Int)

;; Wrong - returns "3000" string
:port #or [#env PORT "3000"]

;; Right - returns 3000 integer
:port #long #or [#env PORT 3000]

Production Variable Not Working

  1. Check file exists and has correct permissions:

    ls -la /home/rakiba/my-app/.env
  2. Check systemd loaded it:

    systemctl show rakiba-my-app --property=Environment
  3. Restart service:

    sudo systemctl restart rakiba-my-app