Deployment guide

VictoriaLogs

Overview

VictoriaLogs is a log database from VictoriaMetrics. It accepts structured and plain text logs over many protocols: its own JSON lines API, the Loki push API, the Elasticsearch bulk API, OpenTelemetry and syslog. It stores them compressed on disk and lets you search and aggregate them with the LogsQL query language, through a built-in web interface (vmui) or plain HTTP.

It is a single binary that needs far less memory and disk than a typical Elasticsearch or OpenSearch stack. An upstream VictoriaLogs binary listens on all interfaces with no authentication. The SecureAnyCloud image closes that gap.

What you get:

  • HTTPS from the first boot. nginx is the only network entry point. It serves the web UI and every ingest and query API on port 443 with a certificate generated on your VM's first boot. Port 80 only redirects to HTTPS, and no login is ever accepted over plain HTTP.
  • A password unique to your VM. Every path except a static /healthz needs HTTP Basic Auth as the user admin, including the delete and internal APIs. The password is generated on first boot and written to the root-only file /root/victorialogs-credentials.txt. nginx stores only its bcrypt hash, and the password is handed to htpasswd on standard input, never on a command line. No password is baked into the image, and no two VMs share one.
  • VictoriaLogs on loopback only. Its HTTP port 9428 is bound to 127.0.0.1 and is never reachable from the network, so never open it in a network security group. The syslog receiver is off.
  • Fail closed. nginx refuses to start until first boot has written the certificate and the password, so VictoriaLogs is never served without a login.
  • Your logs on their own disk. All log data lives on a dedicated 32 GiB Azure data disk mounted at /var/lib/victoria-logs, which you can resize independently of the OS disk. VictoriaLogs will not start on the bare OS disk if that data disk is missing.
  • Fixed host name and real client addresses. Requests for a host name the VM does not serve are dropped. nginx logs the real client address and overwrites client-supplied X-Forwarded-For headers. Cross-site browser requests that change state are refused.
  • Fully patched Ubuntu 24.04 LTS at build time, with unattended security upgrades switched on.

On the image: VictoriaLogs 1.53.0 (/usr/local/bin/victoria-logs-prod), the unmodified upstream community release binary under Apache-2.0 with its checksum verified, plus nginx, Ubuntu 24.04 LTS and the Azure Linux agent. VictoriaLogs runs as its own unprivileged victorialogs user with a 30-day retention period by default. Version 1.53.0 is the upstream security release (Go 1.27.1, POST-only delete and internal endpoints).

Default user: azureuser (or the name you choose) | Recommended VM size: Standard_B2s

VictoriaLogs and VictoriaMetrics are trademarks of their respective owners. This image is provided by SecureAnyCloud and is not affiliated with, endorsed by, or sponsored by VictoriaMetrics.

Prerequisites

  • An active Azure subscription, an SSH key (or a password) and a VNet with a subnet.
  • A network security group that allows TCP 22 (SSH) from your own networks and TCP 443 (the web UI and the ingest and query APIs) from the networks your users and log agents are in. TCP 80 only redirects to HTTPS and is optional. If your agents run inside the same VNet, allow 443 from the VNet address space only. Never open port 9428.
1

Deploy from the Azure Portal

Search the Azure Marketplace for VictoriaLogs by SecureAnyCloud, choose the plan and select Create.

Pick Standard_B2s or larger, and choose your SSH key or password. In the networking step, attach a network security group that allows inbound 22 and 443, and optionally 80, from your own networks only.

The image brings its own 32 GiB data disk for the logs. You can see it on the Disks tab.

2

Deploy from the Azure CLI

Replace the values in angle brackets:

RG="victorialogs-prod"; LOCATION="eastus"; VM_NAME="victorialogs-01" IMAGE_URN="sac1647283583153:victorialogs-ubuntu-24-04:default:latest" az group create --name "$RG" --location "$LOCATION" az network nsg create -g "$RG" --name victorialogs-nsg az network nsg rule create -g "$RG" --nsg-name victorialogs-nsg --name allow-ssh --priority 100 \
  --source-address-prefixes "<your-mgmt-cidr>" --destination-port-ranges 22 --access Allow --protocol Tcp az network nsg rule create -g "$RG" --nsg-name victorialogs-nsg --name allow-https --priority 110 \
  --source-address-prefixes "<your-client-cidr>" --destination-port-ranges 443 80 --access Allow --protocol Tcp az vm image terms accept --urn "$IMAGE_URN" az vm create -g "$RG" --name "$VM_NAME" --image "$IMAGE_URN" --size Standard_B2s \
  --admin-username azureuser --ssh-key-values ~/.ssh/id_rsa.pub \
  --nsg victorialogs-nsg --public-ip-sku Standard
3

Connect and check the services

Connect to the VM over SSH:

ssh azureuser@<vm-public-ip>

First boot generates the certificate and the password, then starts nginx in front of VictoriaLogs. Confirm that the services are active:

systemctl is-active victorialogs nginx victorialogs-firstboot
sudo test -f /var/lib/sac/data/victorialogs-firstboot.done && echo FIRSTBOOT_DONE /usr/local/bin/victoria-logs-prod -version
active active active FIRSTBOOT_DONE victoria-logs-20261001-105650-tags-v1.53.0-0-g915d91904b

Only SSH and nginx listen on public addresses. VictoriaLogs listens on loopback:

sudo ss -Hlntp | awk '{print $4, $6}' | grep -E ':(22|80|443|9428) ' | sed -E 's/,pid=[0-9]+,fd=[0-9]+//g' | sort
0.0.0.0:22 users:(("sshd"),("systemd")) 0.0.0.0:443 users:(("nginx"),("nginx"),("nginx")) 0.0.0.0:80 users:(("nginx"),("nginx"),("nginx")) 127.0.0.1:9428 users:(("victoria-logs-p")) [::]:22 users:(("sshd"),("systemd")) [::]:443 users:(("nginx"),("nginx"),("nginx")) [::]:80 users:(("nginx"),("nginx"),("nginx"))
4

Get your URL and password

The URLs and the user name are in the root-only credentials file. Print them with:

sudo grep -E '^victorialogs\.(url|ingest_url|username)=' /root/victorialogs-credentials.txt
victorialogs.url=https://<vm-public-ip>/select/vmui/ victorialogs.ingest_url=https://<vm-public-ip>/insert/jsonline victorialogs.username=admin

The password is the victorialogs.password line of the same file: sudo grep '^victorialogs.password=' /root/victorialogs-credentials.txt. It is unique to this VM, so store it in your password manager or secrets store.

The URL uses the VM's public IP address from the Azure Instance Metadata Service, or its private IP when it has no public one. The certificate names that address plus the VM's private IP address, so browsers and agents inside your VNet can use either. Inspect the certificate:

openssl x509 -in /etc/ssl/victorialogs/victorialogs.crt -noout -subject -ext subjectAltName -enddate
subject=CN = <vm-public-ip>, O = sac X509v3 Subject Alternative Name: IP Address:<vm-public-ip>, IP Address:<vm-private-ip>, IP Address:127.0.0.1, DNS:localhost notAfter=<date about 27 months after first boot>
5

Check the login

Without the password, every path is refused. A wrong password is refused, and port 80 redirects to HTTPS. With the password, the web UI answers. /healthz answers without a login, for load balancer health probes.

The commands below read the password from the credentials file into a shell variable and pass it to curl through a file descriptor, so it never appears in the process list or your shell history:

H=$(sudo sed -n 's#^victorialogs\.url=https://\([^/]*\)/.*#\1#p' /root/victorialogs-credentials.txt)
PW=$(sudo sed -n 's/^victorialogs\.password=//p' /root/victorialogs-credentials.txt) CA=/etc/ssl/victorialogs/victorialogs.crt curl -s -o /dev/null -w 'http:// -> %{http_code} %{redirect_url}\n' --resolve "$H:80:127.0.0.1" "http://$H/select/vmui/" curl -s -o /dev/null -w 'no login -> HTTP %{http_code}\n' --cacert $CA --resolve "$H:443:127.0.0.1" "https://$H/select/vmui/" curl -s -o /dev/null -w 'wrong password -> HTTP %{http_code}\n' --cacert $CA --resolve "$H:443:127.0.0.1" -u admin:not-the-password "https://$H/select/vmui/" curl -s -o /dev/null -w 'admin + password -> HTTP %{http_code}\n' --cacert $CA --resolve "$H:443:127.0.0.1" -K <(printf 'user = "admin:%s"\n' "$PW") "https://$H/select/vmui/" curl -s -w ' <- /healthz (no login) HTTP %{http_code}\n' --cacert $CA --resolve "$H:443:127.0.0.1" "https://$H/healthz"
http:// -> 301 https://<vm-public-ip>/select/vmui/ no login -> HTTP 401 wrong password -> HTTP 401 admin + password -> HTTP 200 ok <- /healthz (no login) HTTP 200

--resolve sends the request to this VM's own nginx while keeping the real address in the URL, so the certificate check passes. An Azure VM cannot reach its own public IP from inside. From another machine, drop --resolve and use the URL directly.

6

Send logs and query them with LogsQL

The JSON lines API takes one JSON object per line. The query string names the field that holds the message (_msg_field), the timestamp field (_time_field, where 0 means now) and the fields that identify a log stream (_stream_fields).

Send twelve sample lines, then query them back:

H=$(sudo sed -n 's#^victorialogs\.url=https://\([^/]*\)/.*#\1#p' /root/victorialogs-credentials.txt)
PW=$(sudo sed -n 's/^victorialogs\.password=//p' /root/victorialogs-credentials.txt) CA=/etc/ssl/victorialogs/victorialogs.crt for svc in web-01 web-02 db-01; do for lvl in info info warn error; do printf '{"_msg":"%s request handled on %s","_time":"0","service":"checkout","host":"%s","level":"%s"}\n' "$lvl" "$svc" "$svc" "$lvl" done done | curl -s -o /dev/null -w 'ingest -> HTTP %{http_code}\n' --cacert $CA --resolve "$H:443:127.0.0.1" \
  -K <(printf 'user = "admin:%s"\n' "$PW") -H 'Content-Type: application/stream+json' --data-binary @- \
  "https://$H/insert/jsonline?_stream_fields=service,host&_time_field=_time&_msg_field=_msg" sleep 2 curl -s --cacert $CA --resolve "$H:443:127.0.0.1" -K <(printf 'user = "admin:%s"\n' "$PW") \
  --data-urlencode 'query=level:error | fields host, level, _msg | sort by (host)' \
  "https://$H/select/logsql/query" curl -s --cacert $CA --resolve "$H:443:127.0.0.1" -K <(printf 'user = "admin:%s"\n' "$PW") \
  --data-urlencode 'query=service:checkout | stats by (level) count() as lines | sort by (level)' \
  "https://$H/select/logsql/query"
ingest -> HTTP 200 {"host":"db-01","level":"error","_msg":"error request handled on db-01"} {"host":"web-01","level":"error","_msg":"error request handled on web-01"} {"host":"web-02","level":"error","_msg":"error request handled on web-02"} {"level":"error","lines":"3"} {"level":"info","lines":"6"} {"level":"warn","lines":"3"}

The first query filters on a field and picks the fields to return. The second counts lines per level with the stats pipe. The LogsQL documentation lists every filter and pipe.

VictoriaLogs also accepts the Loki push API, so agents already configured for Grafana Loki can send logs here unchanged:

H=$(sudo sed -n 's#^victorialogs\.url=https://\([^/]*\)/.*#\1#p' /root/victorialogs-credentials.txt)
PW=$(sudo sed -n 's/^victorialogs\.password=//p' /root/victorialogs-credentials.txt) CA=/etc/ssl/victorialogs/victorialogs.crt
TS=$(date +%s%N)
curl -s -o /dev/null -w 'loki push -> HTTP %{http_code}\n' --cacert $CA --resolve "$H:443:127.0.0.1" \
  -K <(printf 'user = "admin:%s"\n' "$PW") -H 'Content-Type: application/json' \
  -d "{\"streams\":[{\"stream\":{\"job\":\"guide-test\"},\"values\":[[\"$TS\",\"hello from the Loki push API\"]]}]}" \
  "https://$H/insert/loki/api/v1/push" sleep 2
curl -s --cacert $CA --resolve "$H:443:127.0.0.1" -K <(printf 'user = "admin:%s"\n' "$PW") \
  --data-urlencode 'query=job:guide-test | fields job, _msg' "https://$H/select/logsql/query"
loki push -> HTTP 204 {"job":"guide-test","_msg":"hello from the Loki push API"}
7

Explore logs in the web UI

Open the victorialogs.url from Step 4 (https://<vm-public-ip>/select/vmui/) in a browser. The certificate is self-signed, so the browser warns once. Accept it, or install your own certificate (Step 10). Sign in as admin with the password from the credentials file.

Type a LogsQL query, for example service:checkout, choose a time range and press Execute. The hits histogram shows when the lines arrived, and the Stream fields panel lists the fields that identify your log streams. Results are grouped by stream with color-coded levels.

Narrow the search with a filter such as level:error.

The Table tab shows each line as a row with one column per field, and the JSON tab shows the raw records.

The Overview page summarises everything stored: total lines, ingestion rate, the number of log streams, and the fields with the number of lines that carry each one.

8

Send logs from your servers

Any agent that speaks one of the supported APIs works. Give it the URL, the user admin, the password and the VM's certificate so it can verify the connection. Copy the certificate to each agent host first. It is public, not a secret:

scp azureuser@<vm-public-ip>:/etc/ssl/victorialogs/victorialogs.crt ./victorialogs.crt

Fluent Bit ([OUTPUT] section), sending JSON lines:

[OUTPUT] Name http Match * Host <vm-ip-or-dns-name> Port 443 URI /insert/jsonline?_stream_fields=stream,host&_msg_field=log&_time_field=date Format json_lines Json_date_format iso8601 tls On tls.verify On tls.ca_file /etc/fluent-bit/victorialogs.crt http_User admin http_Passwd <VICTORIALOGS_PASSWORD>

Vector (vector.yaml), using the Elasticsearch-compatible API:

sinks: victorialogs: type: elasticsearch inputs: [your_source] endpoints: ["https://<vm-ip-or-dns-name>/insert/elasticsearch/"] api_version: v8 compression: gzip healthcheck: enabled: false auth: strategy: basic user: admin password: <VICTORIALOGS_PASSWORD> tls: ca_file: /etc/vector/victorialogs.crt query: _msg_field: message _time_field: timestamp _stream_fields: host,container_name

Grafana Alloy, Promtail or any Loki client: use https://<vm-ip-or-dns-name>/insert/loki/api/v1/push as the push URL with basic auth admin and the password. OpenTelemetry Collector: use an otlphttp exporter with logs_endpoint: https://<vm-ip-or-dns-name>/insert/opentelemetry/v1/logs and a basicauth extension.

Agents in the same VNet can use the VM's private IP address, which the certificate also names. Keep the password in the agent's own secret store or a root-only file, not in a world-readable configuration. Agents send no browser Origin header, so the image's cross-site request guard does not affect them.

Syslog (optional). The syslog receiver is off because syslog has no authentication. To receive syslog from hosts in your own network, take these steps:

  • Add -syslog.listenAddr.tcp=:514 to the ExecStart line of /etc/systemd/system/victorialogs.service.
  • Run sudo systemctl daemon-reload && sudo systemctl restart victorialogs.
  • Open TCP 514 in the network security group only to your VNet or VPN address ranges, never to Any or Internet.

Senders then use the VM's private IP address.

9

Storage and retention

VictoriaLogs keeps all log data on the dedicated Azure data disk mounted at /var/lib/victoria-logs:

sudo du -sh /var/lib/victoria-logs df -h /var/lib/victoria-logs | tail -1 | awk '{print $2" total, "$4" free on "$6}' systemctl cat victorialogs | grep -E '^\s+-'
156K /var/lib/victoria-logs 32G total, 30G free on /var/lib/victoria-logs -httpListenAddr=127.0.0.1:9428 \
  -storageDataPath=/var/lib/victoria-logs \
  -retentionPeriod=30d \
  -loggerTimezone=UTC

Logs older than the retention period (30 days) are deleted automatically. To keep them longer, change -retentionPeriod (for example -retentionPeriod=90d or -retentionPeriod=1y) in /etc/systemd/system/victorialogs.service. Then run sudo systemctl daemon-reload && sudo systemctl restart victorialogs. You can also cap the disk space with -retention.maxDiskSpaceUsageBytes.

To grow the data disk, increase its size in the Azure Portal. Open Disks, select the data disk and choose Size + performance. Azure may ask you to stop the VM first. You can also use az disk update --size-gb. Then extend the file system online:

sudo resize2fs "$(findmnt -no SOURCE /var/lib/victoria-logs)"

For backups, take Azure disk snapshots of the data disk or use Azure Backup.

10

Use your own DNS name or certificate

The served host name is fixed by configuration, never taken from a request. To serve VictoriaLogs under your own DNS name, point the name at the VM and run the helper. It records the name, regenerates the self-signed certificate for it, updates the credentials file and reloads nginx:

sudo victorialogs-set-hostname <your-dns-name>

Use a static public IP address or your own DNS name, so the address does not change after a deallocate. A request for a name or address the VM does not serve is dropped. If the dynamic public IP did change, run the helper with the new address.

To use a certificate from your own CA or a public CA instead, replace /etc/ssl/victorialogs/victorialogs.crt (full chain) and /etc/ssl/victorialogs/victorialogs.key (mode 0600, owner root). Then run sudo nginx -t && sudo systemctl reload nginx.

Alternatively, put the VM behind an Azure Application Gateway with your certificate and let it forward to port 443, with /healthz as the health probe path.

11

Security notes

VictoriaLogs runs as its own unprivileged user with a read-only system, and it cannot reach the Azure instance metadata service or the wire server. Check the unit settings:

systemctl show victorialogs -p User -p NoNewPrivileges -p ProtectSystem -p ProtectHome
systemctl show victorialogs -p IPAddressDeny --value | tr ' ' '\n' | sort
User=victorialogs ProtectHome=yes ProtectSystem=full NoNewPrivileges=yes 168.63.129.16/32 169.254.169.254/32

Root logins, empty passwords and X11 forwarding are switched off in /etc/ssh/sshd_config.d/00-sac.conf, which sorts before every other SSH setting. Whether SSH accepts passwords follows the choice you made when you created the VM, SSH key or password. Check the effective values:

sudo sshd -T | grep -E '^(permitrootlogin|permitemptypasswords|x11forwarding|pubkeyauthentication) '
permitrootlogin no pubkeyauthentication yes x11forwarding no permitemptypasswords no

A state-changing request that carries a foreign browser Origin is refused, even with the right password. A web page therefore cannot write to or delete from VictoriaLogs through a signed-in browser:

H=$(sudo sed -n 's#^victorialogs\.url=https://\([^/]*\)/.*#\1#p' /root/victorialogs-credentials.txt)
PW=$(sudo sed -n 's/^victorialogs\.password=//p' /root/victorialogs-credentials.txt) CA=/etc/ssl/victorialogs/victorialogs.crt curl -s -o /dev/null -w 'Origin evil.example ingest -> HTTP %{http_code}\n' --cacert $CA --resolve "$H:443:127.0.0.1" -K <(printf 'user = "admin:%s"\n' "$PW") -H 'Origin: https://evil.example' -H 'Content-Type: application/json' -d '{"_msg":"x"}' "https://$H/insert/jsonline"
Origin evil.example ingest -> HTTP 403

Other points:

  • VictoriaLogs has a single account. Anyone with the password can ingest, read and delete every log stream, so give each team its own VM if they must not see each other's logs.
  • nginx logs the real client address of every request. X-Forwarded-For headers sent by clients are ignored.
  • Restrict port 443 in the network security group to the networks your users and agents use, and never open 9428.
  • The firmware update refresh timer (fwupd-refresh) is masked, because an Azure VM has no firmware that LVFS can update.
  • To rotate the password, generate a new one and write its bcrypt hash with sudo htpasswd -iB /etc/nginx/.victorialogs.htpasswd admin, which reads the new password from standard input. Then update /root/victorialogs-credentials.txt, reload nginx and update your agents.

Support and licensing

VictoriaLogs is open source under the Apache License 2.0. The image ships the unmodified upstream community binary, not the enterprise edition, and there is no per-user or per-CPU licence fee. The image price covers SecureAnyCloud's hardening, first-boot setup and 24/7 support.

  • Email: sales@secureanycloud.com
  • Website: https://secureanycloud.com

VictoriaLogs is developed by VictoriaMetrics. For features and LogsQL, see the VictoriaLogs documentation.

Deploy on Azure

Launch VictoriaLogs on Ubuntu 24.04 LTS on Azure with 24/7 support from SecureAnyCloud.

View this image

Need Help?

Our support team is available 24/7.

sales@secureanycloud.com

Deploy on Azure