From d9b7867dc538d256004fc98eb3d45f390c5afece Mon Sep 17 00:00:00 2001 From: Jaden Scali Date: Tue, 4 Aug 2026 10:25:33 -0400 Subject: [PATCH 01/12] Fix only pushes to main get pushed to prod --- .github/workflows/cicd.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index fbfa433..af46aba 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -44,13 +44,17 @@ jobs: run: echo "SITE_URL=${{ env.SITE_URL }}" >> .env - name: Build Docker Image run: docker build -t jadendocks/ask_easy . + # Only main publishes :latest - name: Login to Docker Hub + if: github.event_name == 'push' && github.ref == 'refs/heads/main' run: docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }} - name: Publish Docker Image to Docker Hub + if: github.event_name == 'push' && github.ref == 'refs/heads/main' run: docker push jadendocks/ask_easy:latest deploy: needs: build + if: github.event_name == 'push' && github.ref == 'refs/heads/main' runs-on: self-hosted steps: - name: Clean up Docker to free disk space @@ -58,6 +62,6 @@ jobs: - name: Pull Image from Docker Hub run: docker pull jadendocks/ask_easy:latest - name: Stop and Remove Existing Container - run: docker rm -f ask_easy + run: docker rm -f ask_easy || true - name: Run Docker Container run: docker run -d -p 3000:3000 --name ask_easy jadendocks/ask_easy:latest From 45350bdbdfaca197629d49d0c93213abf379c280 Mon Sep 17 00:00:00 2001 From: Jaden Scali Date: Tue, 4 Aug 2026 10:40:33 -0400 Subject: [PATCH 02/12] Removed unused CD env var step --- .github/workflows/cicd.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index af46aba..0b1c0b4 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -40,8 +40,6 @@ jobs: run: pnpm test # CD - - name: Create .env file - run: echo "SITE_URL=${{ env.SITE_URL }}" >> .env - name: Build Docker Image run: docker build -t jadendocks/ask_easy . # Only main publishes :latest From 09925315953cd2fd026ba6e3ef7938b9d4065bd5 Mon Sep 17 00:00:00 2001 From: Jaden Scali Date: Tue, 4 Aug 2026 10:45:46 -0400 Subject: [PATCH 03/12] Replace github actions outdated docker login --- .github/workflows/cicd.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index 0b1c0b4..41980c1 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -45,7 +45,10 @@ jobs: # Only main publishes :latest - name: Login to Docker Hub if: github.event_name == 'push' && github.ref == 'refs/heads/main' - run: docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }} + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} - name: Publish Docker Image to Docker Hub if: github.event_name == 'push' && github.ref == 'refs/heads/main' run: docker push jadendocks/ask_easy:latest From 107a7d034637d734f717ea4534306489e4eec25f Mon Sep 17 00:00:00 2001 From: Jaden Scali Date: Tue, 4 Aug 2026 10:48:06 -0400 Subject: [PATCH 04/12] Rename DOCKER_PASSWORD to DOCKER_TOKEN to be more precise --- .github/workflows/cicd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index 41980c1..48d2020 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -48,7 +48,7 @@ jobs: uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} + password: ${{ secrets.DOCKER_TOKEN }} - name: Publish Docker Image to Docker Hub if: github.event_name == 'push' && github.ref == 'refs/heads/main' run: docker push jadendocks/ask_easy:latest From 0d719fa8d67e5df87f7bdcafb2ad1493a639f391 Mon Sep 17 00:00:00 2001 From: Jaden Scali Date: Tue, 4 Aug 2026 13:04:39 -0400 Subject: [PATCH 05/12] Chore dynamically create DB and Redis URLS from existing env vars --- docker-compose.prod.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index d7b8ad1..2d75517 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -13,8 +13,8 @@ services: - ./uploads:/app/uploads env_file: .env environment: - - DATABASE_URL=${DATABASE_URL} - - REDIS_URL=${REDIS_URL} + - DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB} + - REDIS_URL=redis://:${REDIS_PASSWORD}@redis:6379 postgres: environment: From 8ccb54475ca888f89550edee52702dcbe21247ec Mon Sep 17 00:00:00 2001 From: Jaden Scali Date: Tue, 4 Aug 2026 13:19:49 -0400 Subject: [PATCH 06/12] Move whitelists data to env --- Dockerfile | 4 --- admin_whitelist.txt | 8 ------ docker-compose.prod.yml | 2 -- docker-compose.yml | 2 ++ src/app/api/auth/session/route.ts | 2 +- src/lib/adminWhitelist.ts | 31 ++++++++--------------- src/lib/whitelist.ts | 41 +++++++++---------------------- whitelist.txt | 9 ------- 8 files changed, 25 insertions(+), 74 deletions(-) delete mode 100644 admin_whitelist.txt delete mode 100644 whitelist.txt diff --git a/Dockerfile b/Dockerfile index b54ace9..1a68561 100644 --- a/Dockerfile +++ b/Dockerfile @@ -64,10 +64,6 @@ COPY --from=builder --chown=nextjs:nodejs /app/src ./src COPY --from=builder --chown=nextjs:nodejs /app/prisma ./prisma COPY --from=builder --chown=nextjs:nodejs /app/prisma.config.ts ./ -# Whitelist files (needed by auth logic) -COPY --from=builder --chown=nextjs:nodejs /app/admin_whitelist.txt ./ -COPY --from=builder --chown=nextjs:nodejs /app/whitelist.txt ./ - USER nextjs EXPOSE 3000 diff --git a/admin_whitelist.txt b/admin_whitelist.txt deleted file mode 100644 index 66614ee..0000000 --- a/admin_whitelist.txt +++ /dev/null @@ -1,8 +0,0 @@ -# Admin whitelist — god-mode dashboard access -# Format: one UTORid per line -# UTORids listed here can access /dashboard with full read/delete powers. -# Lines starting with # are comments and are ignored. -# Restart the server to pick up changes. -# -# ---- ADMINS ---- -testprof \ No newline at end of file diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 2d75517..0316f40 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -8,8 +8,6 @@ services: volumes: - ./src/server.ts:/app/src/server.ts:ro - ./src/app/api/auth/session/route.ts:/app/src/app/api/auth/session/route.ts:ro - - ./whitelist.txt:/app/whitelist.txt:ro - - ./admin_whitelist.txt:/app/admin_whitelist.txt:ro - ./uploads:/app/uploads env_file: .env environment: diff --git a/docker-compose.yml b/docker-compose.yml index 5598165..5cdff21 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,6 +9,8 @@ services: - DATABASE_URL=postgresql://postgres:postgres@postgres:5432/ask_easy - REDIS_URL=redis://:changeme@redis:6379 - SESSION_SECRET=${SESSION_SECRET} + - PROFESSOR_WHITELIST=${PROFESSOR_WHITELIST} + - ADMIN_WHITELIST=${ADMIN_WHITELIST} - DEV_UTORID=${DEV_UTORID} - DEV_NAME=${DEV_NAME} - DEV_ROLE=${DEV_ROLE} diff --git a/src/app/api/auth/session/route.ts b/src/app/api/auth/session/route.ts index 1756412..9f4d6b1 100644 --- a/src/app/api/auth/session/route.ts +++ b/src/app/api/auth/session/route.ts @@ -66,7 +66,7 @@ export async function GET(request: NextRequest) { // ------------------------------------------------------------------ // 2. Resolve role from the instructor whitelist. - // PROFESSOR / TA → listed in whitelist.txt + // PROFESSOR / TA → listed in PROFESSOR_WHITELIST // STUDENT → everyone else (default) // In dev mode DEV_ROLE overrides the whitelist (for testing instructor UI). // ------------------------------------------------------------------ diff --git a/src/lib/adminWhitelist.ts b/src/lib/adminWhitelist.ts index 5b324f9..663607b 100644 --- a/src/lib/adminWhitelist.ts +++ b/src/lib/adminWhitelist.ts @@ -1,38 +1,27 @@ -import { readFileSync } from "node:fs"; -import { resolve } from "node:path"; - // --------------------------------------------------------------------------- // Admin whitelist // -// Reads `admin_whitelist.txt` (or the path in ADMIN_WHITELIST_PATH env var) -// to determine which UTORids have god-mode dashboard access. -// One UTORid per line; comment lines start with #. +// Reads the ADMIN_WHITELIST env var to determine which UTORids have god-mode +// dashboard access. Comma-separated; surrounding whitespace is ignored. // -// File format (one entry per line): -// utorid -// # comment lines are ignored +// Example: +// ADMIN_WHITELIST=smithj,doejohn // --------------------------------------------------------------------------- function loadAdminWhitelist(): Set { - const whitelistPath = resolve(process.env.ADMIN_WHITELIST_PATH ?? "./admin_whitelist.txt"); + const raw = process.env.ADMIN_WHITELIST; - let contents: string; - try { - contents = readFileSync(whitelistPath, "utf-8"); - } catch { + if (!raw) { console.warn( - `[admin-whitelist] Could not read admin whitelist at ${whitelistPath}. No users will have dashboard access.` + "[admin-whitelist] ADMIN_WHITELIST is not set. No users will have dashboard access." ); return new Set(); } const set = new Set(); - for (const rawLine of contents.split("\n")) { - const line = rawLine.trim(); - if (!line || line.startsWith("#")) continue; - - const utorid = line.split(",")[0].trim().toLowerCase(); + for (const rawEntry of raw.split(",")) { + const utorid = rawEntry.trim().toLowerCase(); if (utorid) set.add(utorid); } @@ -40,7 +29,7 @@ function loadAdminWhitelist(): Set { } // Loaded once at startup (module-level cache). -// Restart the server to pick up changes to admin_whitelist.txt. +// Restart the server to pick up changes to ADMIN_WHITELIST. const ADMIN_WHITELIST: Set = loadAdminWhitelist(); /** diff --git a/src/lib/whitelist.ts b/src/lib/whitelist.ts index 41894d7..3eba110 100644 --- a/src/lib/whitelist.ts +++ b/src/lib/whitelist.ts @@ -1,51 +1,34 @@ -import { readFileSync } from "node:fs"; -import { resolve } from "node:path"; - import type { Role } from "@/utils/types"; // --------------------------------------------------------------------------- // Instructor whitelist // -// Reads `whitelist.txt` (or the path in WHITELIST_PATH env var) to determine -// which UTORids are professors. One UTORid per line; comment lines start with #. +// Reads the PROFESSOR_WHITELIST env var to determine which UTORids are +// professors. Comma-separated; surrounding whitespace is ignored. // -// Any UTORid in the file → PROFESSOR -// Any UTORid NOT in the file → STUDENT +// Any UTORid in the list → PROFESSOR +// Any UTORid NOT in the list → STUDENT // // TAs are assigned per-course by professors via the UI and stored in // CourseEnrollment.role. They are never listed here. // -// File format (one entry per line): -// utorid -// # comment lines are ignored -// // Example: -// smithj -// doejohn +// PROFESSOR_WHITELIST=smithj,doejohn // --------------------------------------------------------------------------- function loadWhitelist(): Set { - const whitelistPath = resolve(process.env.WHITELIST_PATH ?? "./whitelist.txt"); + const raw = process.env.PROFESSOR_WHITELIST; - let contents: string; - try { - contents = readFileSync(whitelistPath, "utf-8"); - } catch { - // If the file doesn't exist, everyone is a student — not a fatal error. - console.warn( - `[whitelist] Could not read whitelist at ${whitelistPath}. All users will be STUDENT.` - ); + if (!raw) { + // If the var is unset, everyone is a student — not a fatal error. + console.warn("[whitelist] PROFESSOR_WHITELIST is not set. All users will be STUDENT."); return new Set(); } const set = new Set(); - for (const rawLine of contents.split("\n")) { - const line = rawLine.trim(); - if (!line || line.startsWith("#")) continue; - - // Tolerate legacy "utorid,PROFESSOR" format — strip anything after a comma - const utorid = line.split(",")[0].trim().toLowerCase(); + for (const rawEntry of raw.split(",")) { + const utorid = rawEntry.trim().toLowerCase(); if (utorid) set.add(utorid); } @@ -53,7 +36,7 @@ function loadWhitelist(): Set { } // Loaded once at startup (module-level cache). -// Restart the server to pick up changes to whitelist.txt. +// Restart the server to pick up changes to PROFESSOR_WHITELIST. const WHITELIST: Set = loadWhitelist(); /** diff --git a/whitelist.txt b/whitelist.txt deleted file mode 100644 index 59a11f3..0000000 --- a/whitelist.txt +++ /dev/null @@ -1,9 +0,0 @@ -# Instructor whitelist -# Format: one UTORid per line -# UTORids listed here are assigned the PROFESSOR role on login. -# Everyone else defaults to STUDENT. -# TAs are assigned per-course by professors via the UI. -# Lines starting with # are comments and are ignored. -# -# ---- TEST ACCOUNTS ---- -testprof From 9d45169713a6f1a151f7d09a1651f3237d8de30d Mon Sep 17 00:00:00 2001 From: Jaden Scali Date: Wed, 5 Aug 2026 14:44:30 -0400 Subject: [PATCH 07/12] Add commit sha label to docker image --- .github/workflows/cicd.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index 48d2020..82f8ac2 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -40,8 +40,13 @@ jobs: run: pnpm test # CD + # The revision label records which commit this image was built from, + # so `docker inspect` on the VM can answer "what is actually running?" - name: Build Docker Image - run: docker build -t jadendocks/ask_easy . + run: | + docker build \ + --label org.opencontainers.image.revision=${{ github.sha }} \ + -t jadendocks/ask_easy . # Only main publishes :latest - name: Login to Docker Hub if: github.event_name == 'push' && github.ref == 'refs/heads/main' From 04a646bfecb5627e2e69c5a4c1b11c7443411bde Mon Sep 17 00:00:00 2001 From: Jaden Scali Date: Wed, 5 Aug 2026 15:32:31 -0400 Subject: [PATCH 08/12] Fix prod compose to work with the new VM runner setup --- docker-compose.prod.yml | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 0316f40..4e29f3b 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -1,15 +1,17 @@ -# Production override - uses pre-built image from Docker Hub -# Usage: docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d +# Production override - runs the pre-built image from Docker Hub. +# The VM never builds; the deploy job only pulls and restarts. +# +# Usage: +# docker compose -p ask_easy --env-file /home/easy/secrets/prod.env \ +# -f docker-compose.yml -f docker-compose.prod.yml up -d services: app: image: jadendocks/ask_easy:latest - build: - context: . volumes: - - ./src/server.ts:/app/src/server.ts:ro - - ./src/app/api/auth/session/route.ts:/app/src/app/api/auth/session/route.ts:ro - - ./uploads:/app/uploads - env_file: .env + # Named volume, not a bind mount: the deploy checkout is wiped on every + # run, so anything stored relative to it would be destroyed. + - uploads_data:/app/uploads + env_file: /home/easy/secrets/prod.env environment: - DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB} - REDIS_URL=redis://:${REDIS_PASSWORD}@redis:6379 @@ -22,3 +24,6 @@ services: command: redis-server --requirepass ${REDIS_PASSWORD} healthcheck: test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"] + +volumes: + uploads_data: From 3d1b393339a468439969be75da60f0db2734e4e6 Mon Sep 17 00:00:00 2001 From: Jaden Scali Date: Wed, 5 Aug 2026 15:40:29 -0400 Subject: [PATCH 09/12] Abort if secret missing in prod --- docker-compose.prod.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 4e29f3b..d445734 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -13,17 +13,17 @@ services: - uploads_data:/app/uploads env_file: /home/easy/secrets/prod.env environment: - - DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB} - - REDIS_URL=redis://:${REDIS_PASSWORD}@redis:6379 + - DATABASE_URL=postgresql://${POSTGRES_USER:?required in prod.env}:${POSTGRES_PASSWORD:?required in prod.env}@postgres:5432/${POSTGRES_DB:?required in prod.env} + - REDIS_URL=redis://:${REDIS_PASSWORD:?required in prod.env}@redis:6379 postgres: environment: - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?required in prod.env} redis: - command: redis-server --requirepass ${REDIS_PASSWORD} + command: redis-server --requirepass ${REDIS_PASSWORD:?required in prod.env} healthcheck: - test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"] + test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD:?required in prod.env}", "ping"] volumes: uploads_data: From d398779eee77761360933c57a5ff1182b01f70c3 Mon Sep 17 00:00:00 2001 From: Jaden Scali Date: Wed, 5 Aug 2026 15:42:28 -0400 Subject: [PATCH 10/12] Use env vars as source of truth for dev testing --- docker-compose.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 5cdff21..3cdfb3e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,8 +6,8 @@ services: ports: - "127.0.0.1:3000:3000" environment: - - DATABASE_URL=postgresql://postgres:postgres@postgres:5432/ask_easy - - REDIS_URL=redis://:changeme@redis:6379 + - DATABASE_URL=postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@postgres:5432/${POSTGRES_DB:-ask_easy} + - REDIS_URL=redis://:${REDIS_PASSWORD:-changeme}@redis:6379 - SESSION_SECRET=${SESSION_SECRET} - PROFESSOR_WHITELIST=${PROFESSOR_WHITELIST} - ADMIN_WHITELIST=${ADMIN_WHITELIST} @@ -32,7 +32,7 @@ services: volumes: - postgres_data:/var/lib/postgresql/data healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres"] + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres}"] interval: 5s timeout: 5s retries: 5 From 115b4371c95332ad1f96dd8ad45537afaab44162 Mon Sep 17 00:00:00 2001 From: Jaden Scali Date: Wed, 5 Aug 2026 15:53:56 -0400 Subject: [PATCH 11/12] Deploy via compose with migrations instead of a bare docker run --- .github/workflows/cicd.yml | 34 ++++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index 82f8ac2..499d06d 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -40,8 +40,7 @@ jobs: run: pnpm test # CD - # The revision label records which commit this image was built from, - # so `docker inspect` on the VM can answer "what is actually running?" + # The revision label records which commit this image was built from - name: Build Docker Image run: | docker build \ @@ -62,12 +61,27 @@ jobs: needs: build if: github.event_name == 'push' && github.ref == 'refs/heads/main' runs-on: self-hosted + timeout-minutes: 20 + env: + # -p pins the project name so the existing named volumes are reused + COMPOSE: >- + -p ask_easy + --env-file /home/easy/secrets/prod.env + -f docker-compose.yml -f docker-compose.prod.yml steps: - - name: Clean up Docker to free disk space - run: docker system prune -af --volumes - - name: Pull Image from Docker Hub - run: docker pull jadendocks/ask_easy:latest - - name: Stop and Remove Existing Container - run: docker rm -f ask_easy || true - - name: Run Docker Container - run: docker run -d -p 3000:3000 --name ask_easy jadendocks/ask_easy:latest + # Only the compose files are used here, the app itself comes from the image + - uses: actions/checkout@v4 + + - name: Reclaim disk (images only) + run: docker image prune -af + - name: Pull new app image + run: docker compose $COMPOSE pull app + - name: Start data services + run: docker compose $COMPOSE up -d --no-build postgres redis + + # Runs before the app is swapped, so a failed migration leaves the + # current version serving + - name: Apply database migrations + run: docker compose $COMPOSE run --rm --no-deps app npx prisma migrate deploy + - name: Recreate app container + run: docker compose $COMPOSE up -d --no-build app From 326eb824b3e12ae17da57264ff9930a0e5e13412 Mon Sep 17 00:00:00 2001 From: Jaden Scali Date: Wed, 5 Aug 2026 16:04:35 -0400 Subject: [PATCH 12/12] Cap prod container logs at 100MB per service --- docker-compose.prod.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index d445734..20524d9 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -4,9 +4,20 @@ # Usage: # docker compose -p ask_easy --env-file /home/easy/secrets/prod.env \ # -f docker-compose.yml -f docker-compose.prod.yml up -d + +# Docker's json-file driver is unbounded by default, and postgres/redis are +# never recreated by a deploy, so their logs would grow until the disk filled. +# 25m x 4 caps each service at 100MB while keeping recent history in chunks. +x-logging: &logging + driver: json-file + options: + max-size: "25m" + max-file: "4" + services: app: image: jadendocks/ask_easy:latest + logging: *logging volumes: # Named volume, not a bind mount: the deploy checkout is wiped on every # run, so anything stored relative to it would be destroyed. @@ -17,10 +28,12 @@ services: - REDIS_URL=redis://:${REDIS_PASSWORD:?required in prod.env}@redis:6379 postgres: + logging: *logging environment: POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?required in prod.env} redis: + logging: *logging command: redis-server --requirepass ${REDIS_PASSWORD:?required in prod.env} healthcheck: test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD:?required in prod.env}", "ping"]