Skip to content

Providing the Demo Video

The public page /video plays the LLARS demo video /videos/llars_demo.mp4. The file is about 88 MB and is deliberately kept out of the repository and out of the frontend image (git- and dockerignored). nginx serves it straight from a directory on the host. On a new host, and whenever /videos/llars_demo.mp4 returns 404, it has to be placed there once by hand.

How to recognise the problem

/video shows the notice "The video can't be loaded right now" with the YouTube backup, curl -I …/videos/llars_demo.mp4 returns 404 Not Found (nginx error page, text/html, 146 bytes), and the smoke test reports WARNING: Demo-Video fehlt auf dem Host.


Where the file lives

What Value
Host path (default) /var/llars-static/videos/llars_demo.mp4
Variable LLARS_STATIC_DIR in /var/llars/.env (default /var/llars-static)
Path inside the nginx container /srv/llars-static/videos/ (read-only)
Mount docker-compose.prod.yml (production) and docker-compose.staging.yml (staging :55080)
nginx location /videos/ in nginx.prod.conf (port 80 and 443) and nginx.prod-no-ssl.conf
Source llars-frontend/public/videos/llars_demo.mp4 on a developer machine

Why outside the checkout

Until 2 October 2026 docker-compose.prod.yml mounted the video from ./static/videos, i.e. /var/llars/static/videos inside the checkout. Every deploy cleans the checkout with git reset --hard and git clean -fd, and git clean removes everything untracked that is not ignored. Because deploy_bluegreen.sh rewrites the tracked docker/nginx/active_upstream.conf on every deploy, the cleanup ran practically every time. On production the directory was therefore empty, stamped with the time of the nightly deploy (02:22).

The default now lives under /var/llars-static, out of reach of any git clean (even with -x) and of a fresh checkout. /static/ is also listed in .gitignore, and the scripts exclude static/ from git clean, so the old path is no longer emptied either.

Blue-green

Blue and green share one checkout under /var/llars; the colours only differ in container names and compose projects. The nginx container (llars_nginx_service) belongs to neither colour. So there is exactly one video directory, and switching colours does not change it.


Placing the file

/var/llars-static sits under /var and needs root rights the first time it is created. Without sudo this works through a short-lived container, the same way deploy_bluegreen.sh runs its chown (Docker creates the missing host directory for -v itself). From a developer machine (SSH alias llars for production, llars-dev for kia-dev; both need the VPN):

scp llars-frontend/public/videos/llars_demo.mp4 llars:/tmp/llars_demo.mp4
ssh llars 'docker run --rm \
  -v /tmp/llars_demo.mp4:/src/llars_demo.mp4:ro \
  -v /var/llars-static:/dst \
  alpine:3.19 sh -c "mkdir -p /dst/videos \
    && cp /src/llars_demo.mp4 /dst/videos/llars_demo.mp4 \
    && chmod 755 /dst /dst/videos && chmod 644 /dst/videos/llars_demo.mp4" \
  && rm -f /tmp/llars_demo.mp4'

With sudo on the host it works directly:

sudo install -D -m 0644 /tmp/llars_demo.mp4 /var/llars-static/videos/llars_demo.mp4
sudo chmod 755 /var/llars-static /var/llars-static/videos

Permissions matter: the nginx worker runs as user nginx inside the container and needs read access to the file (644) and the directories (755).

Once, after the switch of 2 October 2026

If the video was previously placed in /var/llars/static/videos/, it has to be placed in /var/llars-static/videos/ once. Ideally do this before the deploy that brings the switch, so there is no gap. After the deploy, check that nginx mounts the new path:

docker inspect llars_nginx_service \
  --format '{{range .Mounts}}{{.Source}} -> {{.Destination}}{{"\n"}}{{end}}' | grep videos

If it still shows /var/llars/static/videos, recreate the nginx container (command below). The old copy under /var/llars/static/ can go afterwards.

Optional: faster start in the browser

In the current file the moov box (metadata) sits at the end. Browsers fetch it with a range request, which works but takes time on slow connections. ffmpeg -i llars_demo.mp4 -c copy -movflags +faststart llars_demo_fast.mp4 moves the metadata to the front without re-encoding.


Checking

# On the host (emulates the FH gateway, which forwards over HTTP to port 80)
curl -sI -H "X-Forwarded-Proto: https" http://localhost/videos/llars_demo.mp4

# From outside
curl -sI https://llars.e-beratungsinstitut.de/videos/llars_demo.mp4

# Range streaming (seeking, Safari/iOS): expects 206 Partial Content
curl -s -o /dev/null -r 0-0 -w '%{http_code} %{content_type}\n' \
  https://llars.e-beratungsinstitut.de/videos/llars_demo.mp4

Expected: 200 OK (or 206 for the range request), Content-Type: video/mp4 and Accept-Ranges: bytes.

The same check runs in every smoke test (scripts/ci/smoke_test_demo_video.sh, called from smoke_test.sh). If the file is missing it prints a warning with the target path and keeps the job green, because a rollback would not bring the file back. SMOKE_STRICT_VIDEO=1 turns it into a hard failure:

SMOKE_STRICT_VIDEO=1 BASE_URL=http://localhost bash scripts/ci/smoke_test_demo_video.sh

The file is on the host, but nginx still returns 404

There are two possible causes. Either the running container still mounts the old path (see the box above), or the directory was deleted while nginx was running, so the bind mount still points to the deleted directory. Either way the container does not see the file:

docker exec llars_nginx_service ls -la /srv/llars-static/videos/

If it is missing there, recreate the nginx container with the same command deploy_bluegreen.sh uses (it mounts the current active_upstream.conf, so the active colour stays):

cd /var/llars
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --no-deps --force-recreate nginx-service

Staging (:55080) picks up the mount with the next deploy_bluegreen.sh deploy.


Replacing the file

Put a new version in place under the same name. nginx serves the video with expires 7d; browsers that have already loaded it may show the old version from their cache for up to seven days.