deploy: collect container logs into a self-hosted VictoriaLogs
The API logged to stdout and nothing shipped it off the box, so history died with every container recreate and the unbounded json-file driver was a slow disk-fill risk. Vector now reads container logs through the Docker API and forwards them to a VictoriaLogs instance on the same host, with retention and rotation caps driven from the compose env Vector parses each pino JSON line and lifts its fields before shipping, and the app role installs its config alongside the compose file
Pedro Lucas Porcellis porcellis@eletrotupi.com · 14 days ago
c26ff2e888265576d4b65d4b74de89f561690311 Browse filesParents
ee68bbcChanged files
7
Additions
+146
Deletions
-2
ansible/roles/app/defaults/main.yml
1 1
---
2 2
# roles/app/defaults/main.yml
3 3
# Host-side application configuration
4 4
5 5
app_env: production
6 6
# ghcr.io/<owner>/<repo> must match the image the deploy workflow pushes
7 -
app_github_repo: "your-org/nexo"
7 +
app_github_repo: "pedrolucasp/nexo"
ansible/roles/app/files/docker-compose.production.yml
1 1
# /srv/app/production/docker-compose.yml
2 2
# Secrets: cp .env.production.example .env.production and fill in values
3 3
# Start: docker compose up -d
4 -
# Logs: docker compose logs -f api
4 +
# Logs UI: https://<host>/logs/ (nginx proxies it; loopback only otherwise)
5 5
6 6
services:
7 7
db:
8 8
image: postgres:17-alpine
9 9
shm_size: 1g
19 19
POSTGRES_USER: ${POSTGRES_USER}
20 20
POSTGRES_DB: ${POSTGRES_DB}
21 21
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
22 22
volumes:
23 23
- db_data:/var/lib/postgresql/data
24 +
logging: &default-logging
25 +
driver: json-file
26 +
options:
27 +
max-size: "${LOG_MAX_SIZE:-10m}"
28 +
max-file: "${LOG_MAX_FILE:-3}"
24 29
25 30
valkey:
26 31
image: valkey/valkey:9.0-alpine
27 32
restart: unless-stopped
28 33
# No ports exposed
29 34
volumes:
30 35
- valkey_data:/data
31 36
- ./valkey.conf:/etc/valkey/valkey.conf
32 37
command: valkey-server /etc/valkey/valkey.conf
38 +
logging: *default-logging
33 39
34 40
api:
35 41
image: ghcr.io/${GITHUB_REPOSITORY}:${IMAGE_TAG:-latest}
36 42
restart: unless-stopped
37 43
ports:
42 48
depends_on:
43 49
valkey:
44 50
condition: service_started
45 51
db:
46 52
condition: service_healthy
53 +
logging: *default-logging
54 +
55 +
# Logs store — UI at http://127.0.0.1:9428/logs/select/vmui/ (loopback only)
56 +
victorialogs:
57 +
image: victoriametrics/victoria-logs:${VICTORIALOGS_TAG:-v1.53.0}
58 +
command:
59 +
- "-storageDataPath=/vlogs"
60 +
- "-http.pathPrefix=/logs"
61 +
- "-retentionPeriod=${VICTORIALOGS_RETENTION:-14d}"
62 +
- "-retention.maxDiskSpaceUsageBytes=${VICTORIALOGS_MAX_DISK:-2GiB}"
63 +
volumes:
64 +
- vlogs_data:/vlogs
65 +
ports:
66 +
- "127.0.0.1:${VICTORIALOGS_PORT:-9428}:9428"
67 +
restart: unless-stopped
68 +
logging: *default-logging
69 +
70 +
# Reads container logs through the Docker API and ships them to VictoriaLogs
71 +
# `docker compose logs` keeps working: this does not replace the log driver
72 +
vector:
73 +
image: timberio/vector:${VECTOR_TAG:-0.59.X-distroless-libc}
74 +
user: root
75 +
volumes:
76 +
- /var/run/docker.sock:/var/run/docker.sock:ro
77 +
- ./vector.yaml:/etc/vector/vector.yaml:ro
78 +
depends_on:
79 +
- victorialogs
80 +
restart: unless-stopped
81 +
logging: *default-logging
47 82
48 83
volumes:
49 84
db_data:
50 85
valkey_data:
86 +
vlogs_data:
/dev/null → ansible/roles/app/files/vector.yaml
1 +
# Collects Docker container logs and forwards them to VictoriaLogs
2 +
# Bind-mounted by docker-compose.yml (dev) and installed into /srv/app/<env>/
3 +
# by the Ansible `app` role (production)
4 +
sources:
5 +
docker:
6 +
type: docker_logs
7 +
8 +
transforms:
9 +
parse_json_message:
10 +
type: remap
11 +
inputs: [docker]
12 +
# pino writes one JSON object per line — when a line parses we lift its
13 +
# fields to the top level so level/msg/userId become queryable in
14 +
# VictoriaLogs, otherwise the raw line stays as the message
15 +
source: |
16 +
parsed, err = parse_json(.message)
17 +
if err == null && is_object(parsed) {
18 +
. = merge(., parsed) ?? .
19 +
}
20 +
21 +
sinks:
22 +
victorialogs:
23 +
type: elasticsearch
24 +
inputs: [parse_json_message]
25 +
endpoints: ["http://victorialogs:9428/logs/insert/elasticsearch/"]
26 +
mode: bulk
27 +
api_version: v8
28 +
compression: gzip
29 +
healthcheck:
30 +
enabled: false
31 +
request:
32 +
headers:
33 +
VL-Stream-Fields: label.com.docker.compose.service
34 +
VL-Time-Field: timestamp
35 +
VL-Msg-Field: msg,message,_msg
36 +
AccountID: "0"
37 +
ProjectID: "0"
ansible/roles/app/tasks/main.yml
1 1
---
2 2
# roles/app/tasks/main.yml
3 3
# Places the production app config in /srv/app/<env>/
4 4
# Secrets are NOT created here: the .example files are dropped in and you fill
5 5
# them in by hand (that part is deliberately manual, so get ready to SSH and copy it)
39 39
owner: "{{ deploy_user }}"
40 40
group: "{{ deploy_user }}"
41 41
mode: "0640"
42 42
tags: [app]
43 43
44 +
- name: Install the Vector configuration
45 +
ansible.builtin.copy:
46 +
src: vector.yaml
47 +
dest: "/srv/app/{{ app_env }}/vector.yaml"
48 +
owner: "{{ deploy_user }}"
49 +
group: "{{ deploy_user }}"
50 +
mode: "0640"
51 +
tags: [app]
52 +
44 53
- name: Install the environment example files
45 54
ansible.builtin.template:
46 55
src: "{{ item }}.j2"
47 56
dest: "/srv/app/{{ app_env }}/{{ item }}.example"
48 57
owner: "{{ deploy_user }}"
68 77
{{ item.item }} is missing: copy it from {{ item.item }}.example and fill
69 78
it in before the first `docker compose up -d`
70 79
loop: "{{ app_env_stat.results }}"
71 80
when: not item.stat.exists
72 81
tags: [app]
ansible/roles/app/templates/env.example.j2
1 1
# Copy to `.env` in the same directory. `docker compose` loads it automatically
2 2
# to interpolate the ${...} variables used in docker-compose.yml
3 3
# NEVER commit the filled-in file!
4 4
5 5
POSTGRES_USER=orbit
9 9
# ghcr.io/<owner>/<repo> must match the image the deploy workflow pushes
10 10
GITHUB_REPOSITORY={{ app_github_repo }}
11 11
12 12
# CI rewrites IMAGE_TAG on every deploy
13 13
IMAGE_TAG=latest
14 +
15 +
# Logging stack (VictoriaLogs + Vector)
16 +
VICTORIALOGS_TAG=v1.53.0
17 +
VICTORIALOGS_PORT=9428
18 +
VICTORIALOGS_RETENTION=14d
19 +
VICTORIALOGS_MAX_DISK=2GiB
20 +
VECTOR_TAG=0.59.X-distroless-libc
21 +
22 +
# Container log rotation (json-file driver)
23 +
LOG_MAX_SIZE=10m
24 +
LOG_MAX_FILE=3
ansible/roles/app/templates/env.production.example.j2
1 1
# Copy to `.env.production`. Loaded by the api container (env_file)
2 2
# NEVER commit the filled-in file
3 3
4 4
NODE_ENV=production
5 5
PORT=3000
26 26
S3_REGION=""
27 27
S3_URL=""
28 28
S3_ACCESS_KEY=""
29 29
S3_ACCESS_SECRET=""
30 30
S3_BUCKET=""
31 +
32 +
# Image tag — set by CI, or pin manually
33 +
GITHUB_REPOSITORY=pedrolucasp/nexo
34 +
IMAGE_TAG=latest
35 +
36 +
# Logging stack (VictoriaLogs + Vector)
37 +
VICTORIALOGS_TAG=v1.53.0
38 +
VICTORIALOGS_PORT=9428
39 +
VICTORIALOGS_RETENTION=14d
40 +
VICTORIALOGS_MAX_DISK=2GiB
41 +
VECTOR_TAG=0.59.X-distroless-libc
42 +
43 +
# Container log rotation (json-file driver)
44 +
LOG_MAX_SIZE=10m
45 +
LOG_MAX_FILE=3
docker-compose.yml
1 1
services:
2 2
db:
3 3
image: postgres:17-alpine
4 4
shm_size: 1g
5 5
user: postgres
15 15
POSTGRES_USER: user
16 16
POSTGRES_DB: orbit_dev
17 17
POSTGRES_PASSWORD: password
18 18
volumes:
19 19
- db_data:/var/lib/postgresql/data
20 +
logging: &default-logging
21 +
driver: json-file
22 +
options:
23 +
max-size: "${LOG_MAX_SIZE:-10m}"
24 +
max-file: "${LOG_MAX_FILE:-3}"
20 25
21 26
valkey:
22 27
image: valkey/valkey:9.0-alpine
23 28
ports:
24 29
- '6379:6379'
32 37
test: ["CMD", "valkey-cli", "ping"]
33 38
interval: 10s
34 39
timeout: 5s
35 40
retries: 5
36 41
start_period: 5s
42 +
logging: *default-logging
37 43
38 44
api:
39 45
build: ./api
40 46
volumes:
41 47
- ./api:/app
48 54
valkey:
49 55
condition: service_healthy
50 56
db:
51 57
condition: service_healthy
52 58
restart: always
59 +
logging: *default-logging
60 +
61 +
# Logs store — UI at http://localhost:9428/logs/select/vmui/ (loopback only)
62 +
victorialogs:
63 +
image: victoriametrics/victoria-logs:${VICTORIALOGS_TAG:-v1.53.0}
64 +
command:
65 +
- "-storageDataPath=/vlogs"
66 +
- "-http.pathPrefix=/logs"
67 +
- "-retentionPeriod=${VICTORIALOGS_RETENTION:-14d}"
68 +
- "-retention.maxDiskSpaceUsageBytes=${VICTORIALOGS_MAX_DISK:-2GiB}"
69 +
volumes:
70 +
- vlogs_data:/vlogs
71 +
ports:
72 +
- "127.0.0.1:${VICTORIALOGS_PORT:-9428}:9428"
73 +
restart: unless-stopped
74 +
logging: *default-logging
75 +
76 +
# Reads container logs through the Docker API and ships them to VictoriaLogs
77 +
# `docker compose logs` keeps working: this does not replace the log driver
78 +
vector:
79 +
image: timberio/vector:${VECTOR_TAG:-0.59.X-distroless-libc}
80 +
user: root
81 +
volumes:
82 +
- /var/run/docker.sock:/var/run/docker.sock:ro
83 +
- ./ansible/roles/app/files/vector.yaml:/etc/vector/vector.yaml:ro
84 +
depends_on:
85 +
- victorialogs
86 +
restart: unless-stopped
87 +
logging: *default-logging
53 88
54 89
volumes:
55 90
db_data:
56 91
valkey_data:
92 +
vlogs_data: