Centralized Logging for a simple Web App (2026): VictoriaLogs + VMUI + Basic Auth, in One Docker Compose
DevOps

Centralized Logging for a simple Web App (2026): VictoriaLogs + VMUI + Basic Auth, in One Docker Compose

Logs are the “dark matter” of software: invisible when everything works, crucial when it doesn’t. Here’s a clean, low-friction way to collect logs from a PHP container, store them in VictoriaLogs, explore them in VMUI, and protect access with a simple login/password gate.

Stack: VictoriaLogs + VMUI (built-in) + vmauth (Basic Auth) + Vector (collector) + a tiny PHP JSON logger.

Docker logo
Container life, simplified. Image: Docker logo (Apache 2.0) from Wikimedia Commons — source.

The goal: a “good enough” log system you’ll actually use

If you’ve ever SSH’d into a server and grepped a log file at 3am… you already know the problem: logs are everywhere, formats are inconsistent, and the one thing you need is always on the wrong machine.

Centralized logging fixes that. You collect logs from apps and infrastructure into one place, and you get fast search, filters, and a UI for exploring patterns.

We’ll build a minimal “observability lane”:
  • PHP app writes structured logs to stdout (JSON Lines).
  • Vector reads container logs from Docker and forwards them.
  • VictoriaLogs stores logs and exposes VMUI at /select/vmui/.
  • vmauth adds Basic Auth and path-based access control in front of VictoriaLogs.

Why structured logs (JSON Lines) beat “pretty text”

Humans like pretty logs. Machines prefer predictable logs. With JSON Lines (one JSON object per line), you can filter by any field later: level=ERROR, service=api, status=500, user_id=123, and so on.

VictoriaLogs is designed for this kind of log stream: it can index arbitrary fields and query them with LogsQL. That means you don’t need to pre-design a rigid schema the day you start.

Dashboard icon
The “find it fast” part. Image: Dashboard icon (CC BY-SA 3.0) from Wikimedia Commons — source.

Streams: the performance trick most people miss

VictoriaLogs has a concept of streams. During ingestion you can choose a few fields that define a stream (e.g., container name, host, environment). Queries that filter by stream fields get much faster.

The key rule: do not put high-cardinality fields (like request_id) into stream fields. Use stable identifiers such as service, env, host, container_name.

Security: Basic Auth without touching your app code

This is the nice part: you don’t need to implement auth inside VictoriaLogs or your PHP app. Put vmauth in front of VictoriaLogs and publish only vmauth to the outside. VictoriaLogs stays internal to the Docker network.

Log in icon
Basic Auth: low ceremony, solid enough for internal panels. Image: Log-in icon (CC BY-SA 3.0) from Wikimedia Commons — source.

Copy‑paste stack: Docker Compose + configs

The compose below includes four services: victoria-logs, vmauth, vector, and php-app.

Tip: VMUI will be available at http://localhost:8427/select/vmui/ (protected by Basic Auth).

docker-compose.yml

services:
  victoria-logs:
    image: victoriametrics/victoria-logs:latest
    command:
      - -storageDataPath=/vlogs
      - -retentionPeriod=14d
    volumes:
      - vlogs-data:/vlogs
    expose:
      - "9428"

  vmauth:
    image: victoriametrics/vmauth:latest
    command:
      - -auth.config=/etc/vmauth.yml
      - -httpListenAddr=:8427
    volumes:
      - ./vmauth.yml:/etc/vmauth.yml:ro
    ports:
      - "8427:8427"
    depends_on:
      - victoria-logs

  vector:
    image: timberio/vector:latest-alpine
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - ./vector.yaml:/etc/vector/vector.yaml:ro
    depends_on:
      - victoria-logs

  php-app:
    image: php:8.3-cli-alpine
    working_dir: /app
    volumes:
      - ./php-app:/app:ro
    command: ["php", "/app/loggen.php"]

volumes:
  vlogs-data:

vmauth.yml (Basic Auth + allow only /select/*)

users:
  - username: admin
    password: admin123
    url_map:
      - src_paths: ["/select/.*"]
        url_prefix: ["http://victoria-logs:9428"]

vector.yaml (read Docker logs → send JSON Lines to VictoriaLogs)

sources:
  docker_logs:
    type: docker_logs
    exclude_containers: ["victoria-logs", "vmauth", "vector"]

sinks:
  vlogs:
    type: http
    inputs: ["docker_logs"]
    uri: http://victoria-logs:9428/insert/jsonline?_stream_fields=host,container_name&_msg_field=message&_time_field=timestamp
    compression: gzip
    encoding:
      codec: json
    framing:
      method: newline_delimited

php-app/loggen.php (a tiny JSON logger)

<?php

declare(strict_types=1);

function logLine(string $level, string $message, array $context = []): void
{
    $payload = [
        'time' => (new DateTimeImmutable('now', new DateTimeZone('UTC')))->format(DATE_ATOM),
        'level' => $level,
        'message' => $message,
        'service' => 'php-app',
        'env' => 'local',
        'request_id' => bin2hex(random_bytes(8)),
    ] + $context;

    // One JSON object per line (stdout)
    fwrite(STDOUT, json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . PHP_EOL);
}

$levels = ['DEBUG', 'INFO', 'WARN', 'ERROR'];

while (true) {
    $level = $levels[random_int(0, count($levels) - 1)];
    logLine($level, 'demo log line', [
        'path' => '/demo',
        'status' => (string)random_int(200, 599),
    ]);

    usleep(300_000);
}

Run it, then “touch the UI”

From the project folder:

docker compose up -d

Open VMUI (you’ll be prompted for Basic Auth):

http://localhost:8427/select/vmui/

Credentials (from vmauth.yml):

admin / admin123

Starter searches you can paste into VMUI:

_time:5m
error _time:1h
{container_name="php-app"} _time:5m | sort by (_time)

What to improve when you move beyond “demo mode”

  • Real PHP logs: use Monolog / PSR-3 style structured output to stdout (still JSON Lines).
  • Better stream fields: service, env, host, k8s_namespace, container — keep them stable.
  • Separate write/read access: let only collectors hit /insert/*; let humans hit only /select/*.
  • TLS at the edge: put caddy/traefik/nginx in front of vmauth if you need HTTPS.

Further reading (official docs + practical examples)

Quote of the day:

Никто о себе не скажет: я злодей.
By den On January 31, 2026
13

Leave a reply