---
title: "Running WELD off Citadel | WELD docs"
description: "The per-platform contract for running WELD off Citadel: how traffic reaches the app, where logs go, what may be written, which user runs it, how the platform knows it is alive, and what to run after every deploy."
url: https://weld.apps.wayne.edu/docs/running-off-citadel.html
llms: https://weld.apps.wayne.edu/llms.txt
generator: Squire agent-ready
---

# Running WELD off Citadel[#](https://weld.apps.wayne.edu/docs/running-off-citadel.html#running-weld-off-citadel)

WELD deploys on Citadel through Squire: one container per app, and builds run at release time inside the app container. The stack rule (ADR-0010, portable beyond Citadel) says the same app must also run on OpenShift, Coolify and plain Docker. This page is the per-platform contract: how traffic reaches the app, where logs go, what may be written, which user runs it, how the platform knows it is alive, and what to run after every deploy.

Two notes before the tables. Claims marked **(check)** depend on something not built yet:

- There is no production Dockerfile or entrypoint. `compose.yaml` is the local development stack (`artisan serve` on 8000 plus Vite on 5173, SQLite), not a deploy image.
- HELMET (`^0.2`) owns the trusted proxies (BEST_PRACTICES.md, W17). `HELMET_PRESET` picks a platform preset, and HELMET 0.2 ships two: `citadel` and `docker`. An unknown preset name throws at boot. Anywhere else, list the front proxy's address in `HELMET_PROXIES` (it wins over `HELMET_PRESET`), and never trust a whole private range. Laravel's own proxy list has its own variable: set TRUSTED_PROXIES to the same address (unset means Citadel's 172.18.0.1).

## Common ground, every platform[#](https://weld.apps.wayne.edu/docs/running-off-citadel.html#common-ground-every-platform)

| Concern | Contract |
| --- | --- |
| Config | Environment only. No `.env` in the image; `env()` calls live in `config/` (W9). Set `APP_URL` to the real host (W17) and keep `APP_DEBUG=false` in production (W26). |
| Logs | `LOG_CHANNEL=stderr` (`.env.example:18`). Everything goes to `php://stderr`; nothing under `storage/logs` (W14). |
| Writable paths | `storage/` and `bootstrap/cache/` only, plus `/tmp` (W14). |
| Sessions and cache | A shared store, even for one replica: `SESSION_DRIVER=database` (`.env.example:30`) and `CACHE_STORE=database` (`.env.example:40`), because every deploy overlaps old and new containers (W15). |
| User | Non-root. Declare a `USER` in the image; never depend on a `www-data` entry (W13). |
| Health | `/up`, registered in `bootstrap/app.php`: 200 when the framework boots, and it never checks the database, so a database blip cannot kill every replica (W11). |
| After every deploy | Run `php artisan weld:doctor` in the app container. One PASS, WARN or FAIL line per check; exit code 1 when anything FAILs, so it works as a gate. |

## Citadel (for comparison)[#](https://weld.apps.wayne.edu/docs/running-off-citadel.html#citadel-for-comparison)

| Concern | How it works |
| --- | --- |
| Traffic | One container per app, behind Citadel's front-end proxy (HA-Proxy). |
| Trusted proxies | `HELMET_PRESET=citadel`: trust only the proxy in front, never `*` (W17). |
| Logs | `php://stderr`, captured by the platform. File logging is the one case where a path is tolerated here: legacy apps wrote `storage/logs/laravel.log` **(check)**. The portable baseline keeps `storage/logs` empty (W14). |
| Writable paths | `storage/`, `bootstrap/cache/`. |
| User | Set by the Citadel image **(check)**. |
| Health | `/up`. |
| Queue | No queue workers on Citadel. The scheduled-worker mode `WELD_QUEUE_MODE=scheduled` is planned, not built (W19, **check**). |
| After deploy | `php artisan weld:doctor` in the app container. |

## OpenShift[#](https://weld.apps.wayne.edu/docs/running-off-citadel.html#openshift)

| Concern | How it works |
| --- | --- |
| Traffic | The OpenShift router fronts the route and forwards to the pod. |
| Trusted proxies | `HELMET_PROXIES=<router address>`, and the same address in TRUSTED_PROXIES: trust the router only (W17). There is no `openshift` preset; setting one throws. |
| Logs | `php://stderr`; read them with `oc logs`. |
| Writable paths | `storage/`, `bootstrap/cache/`. |
| User | OpenShift assigns a random UID in group 0 with capabilities dropped (W13), so make both writable paths group-owned and group-writable before the pod starts: `chgrp -R 0 storage bootstrap/cache && chmod -R g=u storage bootstrap/cache`. |
| Health | Liveness probe: HTTP GET `/up`. It answers for any `Host` over plain HTTP, before trusted hosts, sessions and auth (W11). |
| After deploy | `oc exec deploy/<app> -- php artisan weld:doctor`. |

## Coolify[#](https://weld.apps.wayne.edu/docs/running-off-citadel.html#coolify)

| Concern | How it works |
| --- | --- |
| Traffic | Coolify's Traefik instance fronts the app and routes by domain. |
| Trusted proxies | `HELMET_PROXIES=<Traefik container address>`, and the same address in TRUSTED_PROXIES (W17). There is no `coolify` preset; setting one throws. |
| Logs | `php://stderr`; Coolify shows the container logs. |
| Writable paths | `storage/`, `bootstrap/cache/`, on a persistent volume so they survive redeploys. |
| User | Whatever the image declares; keep it non-root (W13). |
| Health | A Dockerfile `HEALTHCHECK` that calls `/up` takes precedence over the dashboard check; either way the image needs `curl` or `wget` (W12). Traefik stops routing to an unhealthy container. |
| After deploy | Exec into the app container and run `php artisan weld:doctor`. |

## Plain Docker compose[#](https://weld.apps.wayne.edu/docs/running-off-citadel.html#plain-docker-compose)

| Concern | How it works |
| --- | --- |
| Traffic | Direct to the container, or your own reverse proxy in front. With no proxy, trust no forwarded headers: `HELMET_PRESET=docker` **(check)** (W17). |
| Logs | `php://stderr`; follow them with `docker compose logs -f app`. |
| Writable paths | Mount `storage/` and `bootstrap/cache/` as volumes so data survives the container. |
| User | Non-root `USER` in the image (W13). |
| Health | `HEALTHCHECK` on `/up` in the image (W12). |
| After deploy | `docker compose exec app php artisan weld:doctor`. |

The `compose.yaml` in this repository is the local development stack, not a production image. A production image for raw Docker is **(check)** until one ships.

Four tables, one bead: if the joint passes `weld:doctor` on all of them, the seam holds.
