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.
| Environment | How Env Vars Are Loaded |
|---|---|
| Development | bb clj parses .env and passes to JVM |
| Testing | bb test parses .env same as development |
| Local Sandbox | Docker container with .env uploaded via bb deploy |
| VPS Production | Systemd 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
| File | Committed? | Purpose |
|---|---|---|
.env | NO | Active secrets for local dev |
.env.local | NO | Local overrides |
.env.*.local | NO | Environment-specific local overrides |
.env.example | YES | Template with empty/placeholder values |
.env.encrypted | YES | Encrypted secrets (safe to commit) |
resources/config.edn | YES | Configuration 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
exportprefix - 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
| Tag | Purpose | Example |
|---|---|---|
#env | Read env var (nil if missing) | #env DATABASE_URL |
#or | Fallback chain | #or [#env PORT 3000] |
#profile | Per-environment values | #profile {:dev "localhost" :prod #env DB_HOST} |
#long | Parse as integer | #long #or [#env PORT 3000] |
#keyword | Parse 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:
- Parses
.envfile (KEY=value format) - Passes variables to JVM via
{:extra-env env-vars} - Aero reads them via
#envtags when loadingconfig.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
bb deployreads your local.envfile- Uploads it to
/home/rakiba/<project>/.envinside the container - Creates systemd service with
EnvironmentFiledirective - Service loads env vars at startup
Container Environment
The sandbox provisions a full stack with these services:
| Service | Default Port | Env Vars |
|---|---|---|
| PostgreSQL | 5432 | Credentials in .local-envs/ |
| Valkey | 6379 | ACL credentials |
| Garage (S3) | 3900 | Access keys |
| Datomic | 4334 | Connection URI |
| nginx | 9080 | N/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
- Build: Creates uberjar with
bb build - Upload: Transfers JAR and
.envvia rsync - Systemd: Creates/updates service file
- 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:
- Shell environment -
PORT=8080 bb clj - EnvironmentFile (.env) - Loaded by
bb cljor systemd - Systemd Environment= - Hardcoded in service file
- Aero #env - Reads from combined env
- Aero #profile - Profile-specific defaults
- Aero #or default - Static fallback value
Example
;; config.edn
:port #long #or [#env PORT #profile {:dev 3060 :prod 8080}]
| Scenario | PORT Env | PROFILE | Result |
|---|---|---|---|
bb clj | not set | dev | 3060 |
PORT=9000 bb clj | 9000 | dev | 9000 |
PROFILE=prod bb clj | not set | prod | 8080 |
| VPS (systemd sets PORT=8001) | 8001 | prod | 8001 |
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
- Collect team members’ public keys
- Encrypt with multiple recipients:
age -R alice.pub -R bob.pub -o .env.encrypted .env - 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
| Variable | Required | Default | Description |
|---|---|---|---|
PROFILE | No | dev | Active profile (dev, test, prod) |
PORT | No | 3060 | HTTP server port |
HTTP_HOST | No | 0.0.0.0 | HTTP bind address |
NREPL_PORT | No | 7060 | nREPL port (dev only) |
LOG_LEVEL | No | info | Logging level |
DATABASE_URL | Depends | - | PostgreSQL connection string |
DATOMIC_URI | Depends | - | Datomic connection URI |
S3_ENDPOINT | Depends | - | S3/Garage endpoint |
S3_ACCESS_KEY_ID | Depends | - | S3 access key |
S3_SECRET_ACCESS_KEY | Depends | - | S3 secret key |
S3_BUCKET | No | project name | Default bucket |
VALKEY_HOST | No | localhost | Valkey/Redis host |
VALKEY_PORT | No | 6379 | Valkey/Redis port |
ANTHROPIC_API_KEY | Depends | - | 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
-
Check file exists and has correct permissions:
ls -la /home/rakiba/my-app/.env -
Check systemd loaded it:
systemctl show rakiba-my-app --property=Environment -
Restart service:
sudo systemctl restart rakiba-my-app