-
Notifications
You must be signed in to change notification settings - Fork 2
Environment Variable & Secrets Management
The application needs several secrets and runtime configuration values to operate: Firebase credentials, project IDs, Redis addresses, and public hostnames. These cannot be baked into Docker images (security) and cannot always be passed as files (portability). The solution is a layered pipeline:
´´´ GitHub Secrets │ ▼ build.yml (CI/CD workflow) │ exports secrets as env vars ▼ docker-compose.yml │ injects env vars into each container ▼ entrypoint.sh (Rust containers only) │ decodes FIREBASE_CREDENTIALS_B64 → /tmp/firebase-credentials.json │ exports GOOGLE_APPLICATION_CREDENTIALS ▼ Running process reads env vars at startup ´´´
- GitHub Secrets
- build.yml — CI/CD Workflow
- The Firebase Credentials Problem
- Base64 Encoding — How to Generate FIREBASE_CREDENTIALS_B64
- entrypoint.sh — Decoding at Container Startup
- docker-compose.yml — Variable Injection
- Per-service Variable Reference
- webapp — Build-time Variables
- Rust Services — Runtime Variable Consumption
- Node.js Service — Runtime Variable Consumption
Secrets are stored in the GitHub repository under Settings → Secrets and variables → Actions. They are never written to logs or exposed in workflow output.
| Secret name | What it contains |
|---|---|
SONAR_TOKEN |
SonarQube authentication token for code quality analysis |
FIREBASE_PROJECT_ID |
The Firebase project identifier string (e.g. my-project-12345) |
FIREBASE_CREDENTIALS_B64 |
The full Firebase service account JSON, base64-encoded (see below) |
DEPLOY_HOST |
The public hostname of the production server (e.g. yovi-en2a.com) |
The workflow file lives at .github/workflows/build.yml. It triggers on every push to master and on every pull request.
name: Build
on:
push:
branches:
- master
pull_request:
types: [opened, synchronize, reopened]The single tests job runs on ubuntu-latest and executes in order:
| Step | What it does |
|---|---|
actions/checkout@v4 |
Clones the repository with full history (fetch-depth: 0, required by SonarQube for blame data) |
dtolnay/rust-toolchain@stable |
Installs the stable Rust toolchain with rustfmt and clippy
|
actions/setup-node@v4 |
Installs Node.js 22 |
npm ci (users + webapp) |
Installs dependencies from the lockfile |
npm run test:coverage (users + webapp) |
Runs tests and generates LCOV coverage reports |
cargo install cargo-llvm-cov |
Installs the Rust coverage tool |
cargo llvm-cov |
Generates an LCOV coverage report for the gamey crate |
| Normalize Rust coverage paths | Strips the absolute $GITHUB_WORKSPACE/ prefix from SF: lines so SonarQube can match them to source files |
SonarSource/sonarqube-scan-action |
Uploads all coverage reports to SonarQube |
The SONAR_TOKEN secret is passed to the SonarQube step via the env block:
- name: Analyze with SonarQube
uses: SonarSource/sonarqube-scan-action@master
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}The ${{ secrets.SONAR_TOKEN }} syntax is GitHub Actions' secret interpolation. The value is injected as an environment variable inside that step only — it is never written to the shell history or printed in logs.
Firebase's Node.js and Rust SDKs authenticate via a service account JSON file — a structured JSON object containing a private key, project ID, client email, and other metadata. This file must be available on disk at the path pointed to by the GOOGLE_APPLICATION_CREDENTIALS environment variable.
This creates a problem for containerised deployments: the credentials file cannot be baked into the Docker image (it would be exposed in the image layers and in the registry), and it cannot be mounted as a host volume in portable or cloud deployments where there is no persistent host filesystem.
The solution: encode the JSON file as a base64 string, store the string as a GitHub Secret (and as a Docker Compose variable), pass it into the container as a single environment variable, and decode it back to a file at container startup via entrypoint.sh.
The value stored in the FIREBASE_CREDENTIALS_B64 secret is produced by encoding the service account JSON file with base64. On Linux/macOS:
base64 -w 0 firebase-service-account.jsonThe -w 0 flag disables line wrapping, producing a single unbroken string. This is important because multiline values cannot reliably be stored in environment variables or GitHub Secrets without escaping.
The output looks like: ´´´ eyJwcm9qZWN0X2lkIjoibXktcHJvamVjdCIsInR5cGUiOiJzZXJ2aWNlX2FjY291bnQiLCJ... ´´´
This string is then stored as the FIREBASE_CREDENTIALS_B64 GitHub Secret. It is injected into the auth-engine and game_manager containers at runtime via docker-compose.yml.
Both Rust containers (userAuthentification and game_manager) use an identical entrypoint.sh. It runs as the Docker ENTRYPOINT, before the application binary starts.
#!/bin/sh
if [ -n "$FIREBASE_CREDENTIALS_B64" ]; then
echo "$FIREBASE_CREDENTIALS_B64" | base64 -d > /tmp/firebase-credentials.json
export GOOGLE_APPLICATION_CREDENTIALS=/tmp/firebase-credentials.json
echo "INFO: Firebase credentials written to /tmp/firebase-credentials.json"
else
echo "WARNING: FIREBASE_CREDENTIALS_B64 is not set. Firebase will use default credentials lookup."
fi
exec "$@"| Line | What it does |
|---|---|
[ -n "$FIREBASE_CREDENTIALS_B64" ] |
Checks that the variable is set and non-empty. If missing, the script warns and continues — the Firestore SDK will fall back to its own credentials lookup chain (e.g. Application Default Credentials on GCP) |
echo "$FIREBASE_CREDENTIALS_B64" | base64 -d |
Pipes the base64 string into base64 -d, which decodes it back to the original JSON bytes |
> /tmp/firebase-credentials.json |
Writes the decoded JSON to a temporary file. /tmp is always writable inside a container and is not persisted between restarts |
export GOOGLE_APPLICATION_CREDENTIALS=... |
Sets the standard Google SDK environment variable pointing to the credentials file. Both the Rust firestore crate and the Node.js Firebase SDK read this variable automatically |
exec "$@" |
Replaces the shell process with the application binary (passed as CMD arguments). Using exec instead of a plain call ensures the binary becomes PID 1, so it receives OS signals (e.g. SIGTERM for graceful shutdown) correctly |
COPY entrypoint.sh /entrypoint.sh
RUN sed -i 's/\r$//' /entrypoint.sh && chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
CMD ["./game_manager", "--mode", "server", "--port", "5000"]The sed -i 's/\r$//' strips Windows-style line endings (\r\n) that can be introduced if the file is edited on Windows. Without this, the shell interpreter would fail to parse the script on Linux. chmod +x makes it executable.
When Docker starts the container, it runs /entrypoint.sh ./game_manager --mode server --port 5000. The script decodes the credentials, then exec "$@" hands off to the binary.
docker-compose.yml is the runtime orchestration file. It reads variables from the host shell environment (or a .env file) using the ${VAR_NAME} syntax and injects them into containers.
Variables prefixed with ${} in the environment block are read from the host shell at docker compose up time:
game_manager:
environment:
- FIREBASE_PROJECT_ID=${FIREBASE_PROJECT_ID}
- FIREBASE_CREDENTIALS_B64=${FIREBASE_CREDENTIALS_B64}In production, the deployment server runs docker compose up from a shell that already has these variables exported (set by the CI/CD pipeline or a .env file on the server). Docker Compose substitutes the values at startup.
Variables without ${} are hardcoded directly in the compose file:
users:
environment:
- GAMEMANAGER_URL=http://game_manager:5000
- AUTH_URL=http://auth-engine:4001
- REDIS_HOST=redis_db
- REDIS_PORT=6379These use Docker's internal DNS — containers on the same monitor-net bridge network can reach each other by service name (e.g. redis_db resolves to the Redis container's IP).
The webapp service uses Docker build arguments (ARG) rather than runtime environment variables, because Vite bakes VITE_* variables into the compiled JavaScript bundle at build time:
webapp:
build:
context: ./webapp
args:
- VITE_API_URL=http://localhost:3000These are passed to docker build --build-arg and are only available during the RUN npm run build step — they do not exist in the running container.
| Variable | Source | Required | Description |
|---|---|---|---|
FIREBASE_PROJECT_ID |
Host env / GitHub Secret | ✅ | Firebase project identifier, read by env::var() in firebase.rs
|
FIREBASE_CREDENTIALS_B64 |
Host env / GitHub Secret | ✅ | Base64-encoded service account JSON, decoded by entrypoint.sh
|
GOOGLE_APPLICATION_CREDENTIALS |
Set by entrypoint.sh
|
— | Path to decoded credentials file; read automatically by the Firestore SDK |
| Variable | Source | Required | Description |
|---|---|---|---|
FIREBASE_PROJECT_ID |
Host env / GitHub Secret | ✅ | Firebase project identifier |
FIREBASE_CREDENTIALS_B64 |
Host env / GitHub Secret | ✅ | Base64-encoded service account JSON |
GOOGLE_APPLICATION_CREDENTIALS |
Set by entrypoint.sh
|
— | Path to decoded credentials file |
REDIS_HOST |
Compose static | ✅ | Redis hostname (redis_db in Docker) |
REDIS_PORT |
Compose static | ✅ | Redis port (6379) |
GAMEY |
Compose static | ✅ | Hostname of the game engine container (gamey) |
| Variable | Source | Required | Description |
|---|---|---|---|
REDIS_HOST |
Compose static | ✅ | Redis hostname |
REDIS_PORT |
Compose static | ✅ | Redis port |
AUTH_URL |
Compose static | ✅ | URL of the Auth service |
GAMEMANAGER_URL |
Compose static | ✅ | URL of the Game Manager |
DEPLOY_HOST |
Host env / GitHub Secret | ❌ | Public hostname; added to the CORS allowlist as https://{DEPLOY_HOST}
|
NODE_ENV |
Not set in compose | ❌ | Set to production to enable secure cookies |
| Variable | Source | Required | Description |
|---|---|---|---|
VITE_API_URL |
Build arg | ✅ | Base URL for all API calls, baked into the JS bundle at build time |
DEPLOY_HOST |
Build arg | ✅ | Domain name baked into the nginx config via sed substitution |
CERTBOT_EMAIL |
Compose static | ✅ | Email used by Let's Encrypt for certificate renewal notifications |
The webapp is a static site built by Vite. Environment variables that start with VITE_ are inlined into the JavaScript bundle at build time — they do not exist at runtime and cannot be changed without rebuilding the image.
Inside the webapp/Dockerfile:
ARG VITE_API_URL="http://localhost:3000"
RUN export VITE_API_URL=${VITE_API_URL} && npm run buildThe ARG is promoted to a shell variable with export, making it visible to Vite's build process. Vite replaces every occurrence of import.meta.env.VITE_API_URL in the source code with the literal string value at compile time.
The fallback in the source code handles local development (where no build arg is set):
const API_URL = import.meta.env.VITE_API_URL ?? 'http://localhost:3000';The production nginx config contains a placeholder instead of a hard-coded domain:
ARG DEPLOY_HOST=localhost
COPY nginx.conf /tmp/nginx.conf.template
RUN sed "s/DEPLOY_HOST_PLACEHOLDER/${DEPLOY_HOST}/g" /tmp/nginx.conf.template \
> /etc/nginx/user_conf.d/app.confsed replaces every occurrence of the literal string DEPLOY_HOST_PLACEHOLDER in the template with the value of DEPLOY_HOST. The resulting file is written to the path nginx reads at startup. This allows the same image to be built for any domain without modifying the nginx config manually.
The Rust services read environment variables directly at the point of use with std::env::var(). There is no central config loader — each function reads only what it needs, when it needs it.
// firebase.rs — read on every connection attempt
let project_id = env::var("FIREBASE_PROJECT_ID")
.map_err(|_| "Environment variable FIREBASE_PROJECT_ID is not set")?;
// api_rest.rs — read once at startup
let redis_host = std::env::var("REDIS_HOST").unwrap_or_else(|_| "127.0.0.1".to_string());
let redis_port = std::env::var("REDIS_PORT").unwrap_or_else(|_| "6379".to_string());
let gamey_host = std::env::var("GAMEY").unwrap_or_else(|_| "localhost".to_string());FIREBASE_PROJECT_ID uses the ? operator — if missing, the connection attempt returns an error immediately. The Redis and gamey variables use unwrap_or_else with safe defaults so the service can still start in a local development environment without any environment configuration.
The GOOGLE_APPLICATION_CREDENTIALS variable is never read explicitly in Rust — it is consumed automatically by the firestore crate (via the underlying Google auth library) when the Firestore client is initialised.
users-service.js reads variables at module load time, before any requests are handled:
const GAME_MANAGER_URL = process.env.GAMEMANAGER_URL || 'http://localhost:5000';
const AUTH_URL = process.env.AUTH_URL || 'http://localhost:4001';
const deployHost = process.env.DEPLOY_HOST; // undefined if not setredis-client.js reads Redis connection details at client construction time:
const redisClient = new Redis({
host: process.env.REDIS_HOST || 'localhost',
port: parseInt(process.env.REDIS_PORT || '6379'),
enableOfflineQueue: false,
});All variables fall back to localhost defaults, so the Node.js service can run directly with node users-service.js during development without any configuration. In production, Docker Compose overrides these defaults by injecting the correct values as container environment variables.