Can I self-host Axiom?
YES · ONE COMMAND— setup effort 1 of 4YES — it's called VictoriaLogs. It takes one prompt, a 1024 MB VPS, and about 10 minutes. That is $25 a month you stop paying Axiom — $300 a year on the Axiom Cloud plan, a metered rate, not a whole bill.
Why people pay for Axiom
Stated as the vendor would want it stated. A replacement you pick without knowing what the subscription actually buys is a replacement you abandon in a fortnight.
Axiom sells the part of log management nobody wants to operate: a columnar event store that swallows whatever your services emit without a schema, keeps it for as long as you configure, and answers questions over it in seconds. You are renting ingest capacity, query compute and durable storage as one meter, plus the alerting, dashboards and integrations wrapped around them, and the fact that none of it is your pager at 3am. The bill is usage-shaped rather than seat-shaped, which is honest about where the cost comes from and also means it tracks your traffic rather than your headcount, and the free allowance is large enough that most people meet the pricing page only after something they built started mattering.
| Plan | List price | What it buys |
|---|---|---|
| Personal | free | The page describes it as permanent and needing no credit card, for individual developers and small projects. The allowances printed alongside it are 500 GB per month of data loading, 10 GB-hours of query compute, 25 GB of storage and a 30-day retention window, with full APL access, all integrations and community support. No enterprise add-ons at this level. |
| Axiom Cloudthe plan this page prices against | $25/mo metered | The page prints '$25 / month platform fee + usage' with no minimum commitment. The fee includes an Always Free allowance of 1 TB of data loading, 100 GB-hours of query compute and 100 GB of storage; past that, pay-as-you-go starts automatically and the page says per-unit rates drop at higher volume tiers so the total grows sub-linearly. Retention is configurable. Governance features are separately priced self-serve add-ons: SSO (SAML) at $100/mo, Directory Sync (SCIM) at $100/mo, Role-Based Access Control at $50/mo and Audit Log at $50/mo. Spend alerts and hard caps are set in-console, and pre-purchased credits buy up to 30 percent off. The $25 is a floor, not a bill. |
Vendor list prices in USD, read from the pricing page on 2026-08-14 · confidence: medium
Replaced by VictoriaLogs
One project, named before the prompt, so you know what you are about to install.
One container that ingests, stores and queries your logs, on a disk you own, with no per-gigabyte meter.
One container, one port, one directory: it ingests over the same Splunk, Elasticsearch, Loki and OpenTelemetry protocols your collectors already speak, stores logs in a columnar format on your own disk, and ships a query interface and query API of its own so nothing else has to be installed to read them. Docker's built-in splunk log driver points at it with four lines of compose, which makes the core loop reachable in one sitting. What it does not replace is the rest of the meter: there are no traces, no metrics, no dashboards and no alerting in the box, and the retention promise becomes a number you chose and a disk you watch. That is the trade, and it is why this is a one-command install with an ongoing habit attached.
The swap
You'd run
VictoriaLogs
ONE COMMAND · ~10 min to running · 1024 MB RAM
Axiom Axiom Cloud · a metered rate, not a whole bill · vendor list price · checked 2026-08-14 · source · confidence: medium
Before you start
- RAM floor
- 1024 MBfloor from upstream docs — not measured by us yet
- Disk
- 20 GBthe app, its data, and room for one backup
- Domain needed
- yes, one A recorda hostname pointed at the box before you start — TLS needs it on the cloud path, and the local path needs none
- Time budget
- ~10 minunder 10 minutes, through the first backup
The prompt
Two paths to the same VictoriaLogs: the cloud one assumes Prompt Zero is done on a server you rent, the local one assumes nothing but a computer that can run Docker Desktop. Read whichever you pick before you paste it, which is the whole reason both are on the page instead of behind a download.
Where it runs
319 lines · 14,997 bytes
What this prompt will do
- Preflight
- Layout
- Secrets
- compose.yml
- Caddy and TLS
- Firewall
- Start and verify
- First backup and restore
- Updating later
- What will probably go wrong
- Out of scope
Read out of the prompt’s own step headings at build time — if the prompt changes, this list changes with it.
You are Claude Code on the user's machine. The user has completed Prompt Zero: `ssh vps` works,
Docker and Caddy are installed, the firewall is default-deny.
Run every command in this prompt on the server over `ssh vps` unless the step says otherwise.
Install VictoriaLogs 1.52.0 on that server, reachable at https://<DOMAIN>, behind the existing
Caddy with automatic TLS.
## 1. Preflight
If `<DOMAIN>` is still literal, ask the user for the hostname once and stop until they answer.
Its A record must already point at this server.
Say three things first. VictoriaLogs has no accounts and no sign-in form, so a public hostname
with nothing in front hands every log line this box keeps to whoever loads the URL. Step 7 does not
finish until real container output has been shipped in. And this is logs only: no metrics, no
traces, no performance monitoring, no alert rules.
VictoriaLogs needs 1024 MB of RAM available and 20 GB free on /srv. The image publishes amd64,
arm64, arm/v7 and ppc64le. Measure all four, and confirm Caddy is 2.8 or newer:
```bash
free -m | awk '/^Mem:/ {print $7 " MB available of " $2 " MB"}'
df -BG --output=avail /srv | tail -1
dpkg --print-architecture
dig +short <DOMAIN>
caddy version
```
If available RAM is under 1024 MB or free disk is under 20 GB, print both numbers and stop. Do not
install and hope. If `dig +short` prints nothing, print that and stop. If Caddy predates 2.8, stop:
this install uses the `basic_auth` spelling that arrived there. The disk floor is high for one
container because a log store fills disk: this one caps data at 5 GiB, backups land beside it.
## 2. Layout
```bash
sudo install -d -m 750 -o $(id -u) -g $(id -g) /srv/victorialogs /srv/victorialogs/backups /srv/victorialogs/data
ls -la /srv/victorialogs
```
Assert: `data` and `backups` exist and are owned by the login user. `data` is the only thing the
container writes: it becomes `/vlogs` inside, and every ingested line lands there in a per-day
partition directory. No config directory, because there is no config file.
## 3. Secrets
One secret: the password Caddy checks before any request reaches VictoriaLogs. Generate it on the
server. Do not print it, repeat it in your summary, or put it in a log line.
```bash
umask 077
openssl rand -hex 24 > /srv/victorialogs/dashboard-password
chmod 600 /srv/victorialogs/dashboard-password
umask 022
ls -la /srv/victorialogs/dashboard-password
```
Assert: the file is mode `-rw-------`. Tell the user the browser login name is `vlogs`, that they
read the password with `sudo cat /srv/victorialogs/dashboard-password`, and that both belong in
their password manager now. Step 5 hashes it for Caddy; the plaintext stays because the restore
needs it. VictoriaLogs ships no admin account and no API token, so there is no second one.
## 4. compose.yml
```bash
cat > /srv/victorialogs/compose.yml <<'EOF'
# VictoriaLogs · the deterministic fallback. Authored by caniselfhostit from
# the upstream documentation, not copied from a repository:
# docker image ... https://docs.victoriametrics.com/victorialogs/quickstart/
# flags .......... https://docs.victoriametrics.com/victorialogs/
# log driver ..... https://docs.victoriametrics.com/victorialogs/data-ingestion/splunk/
#
# One service, configured entirely by the `command:` list: there is no
# configuration file and no .env. Upstream's own demo under deployment/docker
# runs seven services; this keeps the one that stores and answers queries. No
# healthcheck: the image is distroless, so every assert runs from the host, and
# no login screen, so Caddy basic_auth is the door. Plain v1.52.0 is the open
# source build; step 9 covers the -enterprise tags. Digest read from Docker Hub
# on 2026-08-14; amd64, arm64, arm/v7 and ppc64le.
#
# NOT YET VERIFIED: no harness run has been recorded against this file.
services:
victorialogs:
image: victoriametrics/victoria-logs:v1.52.0@sha256:47b820890d64c4575a2a0a46415dcd8a4fd59a0f1fcd6a377693d7aea639442e
container_name: victorialogs
restart: unless-stopped
command:
# Absolute, so the mount below is the only place logs can land.
- "-storageDataPath=/vlogs"
# Upstream's default retention is 7 days. Thirty is the choice here.
- "-retentionPeriod=30d"
# The ceiling that protects the disk: past this the oldest per-day
# partitions drop, whatever the retention period says.
- "-retention.maxDiskSpaceUsageBytes=5GiB"
volumes:
# Every ingested line lands here, in per-day partition directories.
- /srv/victorialogs/data:/vlogs
ports:
# Loopback only: Caddy from outside, the Docker log driver from inside.
- "127.0.0.1:8190:9428"
EOF
cd /srv/victorialogs && docker compose config >/dev/null && echo "compose OK"
```
Assert: that prints `compose OK`. One service, one port, one bind mount.
## 5. Caddy and TLS
The credential Caddy checks is a bcrypt hash of step 3's password, read from the file rather than
from an argument, so it never reaches the process list:
```bash
umask 077
caddy hash-password < /srv/victorialogs/dashboard-password > /srv/victorialogs/auth.hash
printf 'basic_auth {\n\tvlogs %s\n}\n' "$(cat /srv/victorialogs/auth.hash)" > /srv/victorialogs/auth.conf
umask 022
sudo install -m 640 -o root -g caddy /srv/victorialogs/auth.conf /etc/caddy/victorialogs-auth.conf
rm -f /srv/victorialogs/auth.hash /srv/victorialogs/auth.conf
sudo grep -c basic_auth /etc/caddy/victorialogs-auth.conf
```
Assert: that prints `1`. A `0` means `caddy hash-password` wrote nothing and the site block below
would publish an open log store, so stop there.
Then the site block, with `<DOMAIN>` replaced by the hostname from step 1. Copy the Caddyfile
first: a syntax error takes down every other site on this box.
```bash
sudo cp /etc/caddy/Caddyfile /etc/caddy/Caddyfile.before-victorialogs
printf '\n' | sudo tee -a /etc/caddy/Caddyfile >/dev/null
sudo tee -a /etc/caddy/Caddyfile >/dev/null <<'EOF'
# VictoriaLogs · the Caddy site block for this service.
#
# Authored by caniselfhostit from
# https://docs.victoriametrics.com/victorialogs/security-and-lb/
# https://caddyserver.com/docs/automatic-https and
# https://caddyserver.com/docs/caddyfile/directives/basic_auth
#
# Append this to /etc/caddy/Caddyfile, with <DOMAIN> replaced by the hostname
# pointed at this box. VictoriaLogs has no accounts and no sign-in form, and
# its one optional Basic Auth flag pair still leaves /health, /ping and
# /robots.txt open, so the door belongs here, where it covers every path.
# Needs Caddy 2.8 or newer.
<DOMAIN> {
encode zstd gzip
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options "nosniff"
X-Frame-Options "SAMEORIGIN"
Referrer-Policy "no-referrer"
-Server
}
# Credential lives in /etc/caddy/victorialogs-auth.conf (not published here).
import /etc/caddy/victorialogs-auth.conf
# 8190 is the loopback port compose publishes. It is not a container port
# and it is not open in the firewall.
reverse_proxy 127.0.0.1:8190
}
EOF
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
```
Assert: validate and reload both exit 0. If validate fails, restore
/etc/caddy/Caddyfile.before-victorialogs, reload, and report what it objected to.
## 6. Firewall
```bash
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 443/udp
sudo ufw status verbose
```
80/tcp answers the ACME challenge and redirects to HTTPS, 443/tcp is the only way in, 443/udp is
HTTP/3. 8190 stays closed: it is bound to 127.0.0.1 and Caddy reaches it over loopback. Assert:
`Status: active`, rules for 80, 443/tcp and 443/udp, no rule for 8190 or 9428. Say plainly that any
process on this box reaches 127.0.0.1:8190 with no password. That is what lets step 7's log driver
write, and it is the edge of a single-tenant box.
## 7. Start and verify
VictoriaLogs creates its storage on first start. No migration step, no wizard, no first account.
```bash
cd /srv/victorialogs
docker compose pull
docker compose up -d
for i in $(seq 1 30); do code=$(curl -sS -o /dev/null -w '%{http_code}' http://127.0.0.1:8190/health); echo "$i $code"; [ "$code" = 200 ] && break; sleep 5; done
printf '{"_msg":"caniselfhostit install check","service":"install-check"}\n' | curl -sS -o /dev/null -w '%{http_code}\n' -X POST -H 'Content-Type: application/stream+json' --data-binary @- 'http://127.0.0.1:8190/insert/jsonline?_stream_fields=service'
sleep 3
curl -sS http://127.0.0.1:8190/select/logsql/query -d 'query={service="install-check"}' | grep -c 'caniselfhostit install check'
curl -sS -o /dev/null -w '%{http_code}\n' https://<DOMAIN>/select/vmui/
curl -sS -o /dev/null -w '%{http_code}\n' -u "vlogs:$(sudo cat /srv/victorialogs/dashboard-password)" https://<DOMAIN>/select/vmui/
```
Assert all five and print what you received. The loop ends on `200`, `/health` answering `OK`. The
ingest call prints `200`. The query prints `1`: a line went in over the JSON stream API and came
back out through LogsQL, which is the whole product. The unauthenticated public call prints `401`,
the security assert in this block; the authenticated one prints `200`. If any of the five misses,
stop, run `docker compose logs --tail 40 victorialogs`, and name the cause. A `502` with a running
container is Caddy. A `200` where `401` was expected means step 5's import line did not load and the
store is public. A `0` after a `200` ingest means the stream field does not match the JSON. A
running container is not success.
STOP: tell the user to open https://<DOMAIN>/select/vmui/ in a private window, sign in as `vlogs` with the password from /srv/victorialogs/dashboard-password, and confirm the query `*` returns the `caniselfhostit install check` line. Do not continue until they confirm.
Now ship real logs in. Docker's built-in `splunk` log driver writes to the Splunk HTTP Event
Collector paths VictoriaLogs answers, so nothing more is installed. Run
`docker ps --format '{{.Names}}'`. If nothing but `victorialogs` is listed, print the block below,
say where it goes later, and go to step 8. Otherwise:
STOP: ask the user which service should ship its output, and wait. Do not continue until they confirm.
Add that block to the service they name, in its own compose file, keep the keys already there, then
`docker compose up -d --force-recreate <service>`:
```yaml
logging:
driver: splunk
options:
splunk-url: "http://127.0.0.1:8190"
splunk-token: "PLACEHOLDER"
splunk-verify-connection: "false"
tag: "{{.Name}}"
```
The token is required by the driver and ignored by VictoriaLogs, which reads no token there. The
connection check is off on purpose: the driver probes with an OPTIONS request at start-up and
expects `200`, VictoriaLogs answers every OPTIONS request with `204`, and a container whose probe
fails never starts. Never put this block on `victorialogs` itself. Then prove the loop:
```bash
before=$(curl -sS http://127.0.0.1:8190/select/logsql/query -d 'query=_time:10m' | wc -l)
sleep 20
after=$(curl -sS http://127.0.0.1:8190/select/logsql/query -d 'query=_time:10m' | wc -l)
echo "before=$before after=$after"
curl -sS http://127.0.0.1:8190/select/logsql/query -d 'query=_time:2m' | head -2
```
Assert: `after` is greater than `before`, and the last command prints a line of that service's own
output. If the two are equal the driver is not delivering: run
`docker inspect --format '{{.HostConfig.LogConfig}}' <container>` and check the URL.
## 8. First backup and restore
One archive: the log partitions, the compose file, the Caddy password, the live Caddyfile and the
auth conf. The container stops for it: a storage directory copied mid-merge is not a backup.
Downtime is a few seconds.
```bash
cd /srv/victorialogs
docker compose stop
sudo tar -czf /srv/victorialogs/backups/victorialogs-$(date +%F).tar.gz -C /srv/victorialogs data compose.yml dashboard-password -C /etc/caddy Caddyfile victorialogs-auth.conf
docker compose start
ls -lh /srv/victorialogs/backups/
```
Assert: the archive exists and is non-empty. Print its size. The only credential in it is
`dashboard-password`, so it is secret material. A backup on the same disk is not a backup, so run
this from the user's machine:
```bash
mkdir -p ~/backups/victorialogs
scp vps:/srv/victorialogs/backups/*.tar.gz ~/backups/victorialogs/
```
To restore cold on a box that has been through Prompt Zero: `docker compose down`, recreate the
directories as in step 2, untar into /srv/victorialogs, move `Caddyfile` and
`victorialogs-auth.conf` back under /etc/caddy, `sudo systemctl reload caddy`, then
`docker compose up -d`. Re-run step 7's `401` and `200` asserts. `data/` is every line kept and
`dashboard-password` is how the user gets back in, so missing the second is a lockout.
## 9. Updating later
New versions are at https://github.com/VictoriaMetrics/VictoriaLogs/releases, changelog at
https://docs.victoriametrics.com/victorialogs/changelog/. Stay on the plain tag: `-enterprise` and
`-enterprise-fips` in the same Docker Hub repository are a separate commercial build under its own
licence rather than the Apache-2.0 one, and they expect a licence key. Take a backup, then edit the
image line in compose.yml to the new tag and digest:
```bash
cd /srv/victorialogs
docker compose pull
docker compose up -d
docker compose logs --tail 30 victorialogs
```
Re-run step 7's checks first. Two numbers in `command:` are worth revisiting then:
`-retentionPeriod=30d` decides how far back a query reaches, and
`-retention.maxDiskSpaceUsageBytes=5GiB` drops the oldest days when the disk fills first. Raise
either, then watch `du -sh /srv/victorialogs/data`.
## 10. What will probably go wrong
The install will look finished and the product will be empty. I had a green health check, a signed
certificate, a working login box, and a query screen answering every question with zero results, and
I spent twenty minutes assuming my query language was wrong. Nothing was wrong. Nothing was
shipping. A log store is the one service where a correct install and a useless one look identical
from outside, because the useful half is configuration on containers you are not installing today.
The other failure: a container refuses to start after you attach the log driver, saying it failed to
initialize the logging driver. That is the connection probe this prompt turns off. Leave it on, or
aim the driver where VictoriaLogs is not listening, and the container never runs.
## 11. Out of scope
- Do not add Grafana, Vector, vmauth, vmalert or Alertmanager. Upstream's demo runs all five;
this install is the log store alone.
- Do not set `-httpAuth.username` or `-httpAuth.password` on the container. Caddy is the door; a
second half-covering one only looks like protection.
- Do not open 8190 in the firewall or rebind it to 0.0.0.0.
- Do not configure alerting rules. They need vmalert plus a notifier, a second install.No terminal agent? Use the chat fallback — slower, you paste the commands
For ChatGPT or Claude in a browser. The model cannot touch your server, so it hands you one command at a time and you run each one. Same install, more of your evening.
This path is slower: you paste every command yourself, and there is nobody watching the output
but you. If you can run Claude Code, use the other tab.
You are installing VictoriaLogs 1.52.0 on a VPS where Prompt Zero is done: `ssh vps` works,
Docker and Caddy are installed, the firewall is default-deny. Run everything over `ssh vps`
unless a step says otherwise, and replace `<DOMAIN>` with the hostname whose A record already
points at the box.
Read these three before step 1, because together they decide whether you want this. VictoriaLogs
has no accounts, no sign-in form and no roles, so a public hostname with nothing in front of it
hands every log line this box keeps to whoever loads the URL; step 3 and step 5 put a password in
Caddy and step 7 asserts it. Step 7 also does not finish until real container output is shipping
in, because an empty log database is not a log store and the shipping half is configuration on
containers this install does not touch. And this is logs only: no metrics, no traces, no
application performance monitoring, no alert rules.
## 1. Preflight
```bash
free -m | awk '/^Mem:/ {print $7 " MB available of " $2 " MB"}'
df -BG --output=avail /srv | tail -1
dpkg --print-architecture
dig +short <DOMAIN>
caddy version
```
You should see: at least `1024` MB available, at least `20` GB free on /srv, `amd64` or `arm64`
(the image also publishes arm/v7 and ppc64le), your server's IP address, and a Caddy version of
2.8 or newer.
If you do not: under-floor RAM or disk means stop rather than install and hope, and the disk floor
is high for one container because a log store is the thing that fills a disk. This install caps its
own data directory at 5 GiB, upstream asks for spare space around it, and the backup archives in
step 8 land on the same disk until you move them off, which is where the other 15 GB goes. An
empty `dig` answer means the A record has not been created or has not propagated; wait a few
minutes and run it again. A Caddy older than 2.8 does not know the `basic_auth` spelling this
install uses, so upgrade Caddy before going on rather than rewriting the site block.
## 2. Layout
```bash
sudo install -d -m 750 -o $(id -u) -g $(id -g) /srv/victorialogs /srv/victorialogs/backups /srv/victorialogs/data
ls -la /srv/victorialogs
```
You should see: `data` and `backups`, both owned by your login user. `data` becomes `/vlogs`
inside the container and holds every ingested line in per-day partition directories.
If you do not: a permission error means you are not in the sudo group, which Prompt Zero set up.
There is no config directory to create, because VictoriaLogs has no configuration file.
## 3. Secrets
One secret: the password Caddy checks before any request reaches VictoriaLogs.
```bash
umask 077
openssl rand -hex 24 > /srv/victorialogs/dashboard-password
chmod 600 /srv/victorialogs/dashboard-password
umask 022
ls -la /srv/victorialogs/dashboard-password
```
You should see: `-rw-------` on that file. Read it with
`sudo cat /srv/victorialogs/dashboard-password`. The login name for the browser box is `vlogs`.
Put both in your password manager now.
If you do not: a mode other than 600 means the `umask` line did not run, so re-run `chmod 600`
before going on.
Do not paste the contents of that file, or any command output containing it, back into this chat
window. Nothing on this path needs the value except your browser and step 7's own `curl`, and a
password pasted into a chat window belongs to whoever runs that chat window.
## 4. compose.yml
```bash
cat > /srv/victorialogs/compose.yml <<'EOF'
# VictoriaLogs · the deterministic fallback. Authored by caniselfhostit from
# the upstream documentation, not copied from a repository:
# docker image ... https://docs.victoriametrics.com/victorialogs/quickstart/
# flags .......... https://docs.victoriametrics.com/victorialogs/
# log driver ..... https://docs.victoriametrics.com/victorialogs/data-ingestion/splunk/
#
# One service, configured entirely by the `command:` list: there is no
# configuration file and no .env. Upstream's own demo under deployment/docker
# runs seven services; this keeps the one that stores and answers queries. No
# healthcheck: the image is distroless, so every assert runs from the host, and
# no login screen, so Caddy basic_auth is the door. Plain v1.52.0 is the open
# source build; step 9 covers the -enterprise tags. Digest read from Docker Hub
# on 2026-08-14; amd64, arm64, arm/v7 and ppc64le.
#
# NOT YET VERIFIED: no harness run has been recorded against this file.
services:
victorialogs:
image: victoriametrics/victoria-logs:v1.52.0@sha256:47b820890d64c4575a2a0a46415dcd8a4fd59a0f1fcd6a377693d7aea639442e
container_name: victorialogs
restart: unless-stopped
command:
# Absolute, so the mount below is the only place logs can land.
- "-storageDataPath=/vlogs"
# Upstream's default retention is 7 days. Thirty is the choice here.
- "-retentionPeriod=30d"
# The ceiling that protects the disk: past this the oldest per-day
# partitions drop, whatever the retention period says.
- "-retention.maxDiskSpaceUsageBytes=5GiB"
volumes:
# Every ingested line lands here, in per-day partition directories.
- /srv/victorialogs/data:/vlogs
ports:
# Loopback only: Caddy from outside, the Docker log driver from inside.
- "127.0.0.1:8190:9428"
EOF
cd /srv/victorialogs && docker compose config >/dev/null && echo "compose OK"
```
You should see: `compose OK`. One service, one published port, one bind mount.
If you do not: `docker compose config` prints the line it objected to. A YAML error is almost
always an indentation change made while pasting, so paste the block again in one piece rather than
editing it line by line. If a later `docker compose pull` complains that the manifest digest does
not match, the tag and the digest in that image line have drifted apart, which means the line was
edited by hand; put both back exactly as written above rather than dropping the `@sha256:` part.
## 5. Caddy and TLS
First the credential Caddy checks, a bcrypt hash of the password from step 3. It is read from the
file rather than typed as an argument, so it never reaches the process list:
```bash
umask 077
caddy hash-password < /srv/victorialogs/dashboard-password > /srv/victorialogs/auth.hash
printf 'basic_auth {\n\tvlogs %s\n}\n' "$(cat /srv/victorialogs/auth.hash)" > /srv/victorialogs/auth.conf
umask 022
sudo install -m 640 -o root -g caddy /srv/victorialogs/auth.conf /etc/caddy/victorialogs-auth.conf
rm -f /srv/victorialogs/auth.hash /srv/victorialogs/auth.conf
sudo grep -c basic_auth /etc/caddy/victorialogs-auth.conf
```
You should see: `1`.
If you do not: a `0` means `caddy hash-password` wrote nothing, and the site block below would
then publish an open log store to the internet. Stop here and re-run this block before going on.
Now the site block. Replace `<DOMAIN>` with your hostname before you paste, and note that the
first command keeps a copy of the working Caddyfile, because a syntax error here takes down every
other site on the box:
```bash
sudo cp /etc/caddy/Caddyfile /etc/caddy/Caddyfile.before-victorialogs
printf '\n' | sudo tee -a /etc/caddy/Caddyfile >/dev/null
sudo tee -a /etc/caddy/Caddyfile >/dev/null <<'EOF'
# VictoriaLogs · the Caddy site block for this service.
#
# Authored by caniselfhostit from
# https://docs.victoriametrics.com/victorialogs/security-and-lb/
# https://caddyserver.com/docs/automatic-https and
# https://caddyserver.com/docs/caddyfile/directives/basic_auth
#
# Append this to /etc/caddy/Caddyfile, with <DOMAIN> replaced by the hostname
# pointed at this box. VictoriaLogs has no accounts and no sign-in form, and
# its one optional Basic Auth flag pair still leaves /health, /ping and
# /robots.txt open, so the door belongs here, where it covers every path.
# Needs Caddy 2.8 or newer.
<DOMAIN> {
encode zstd gzip
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options "nosniff"
X-Frame-Options "SAMEORIGIN"
Referrer-Policy "no-referrer"
-Server
}
# Credential lives in /etc/caddy/victorialogs-auth.conf (not published here).
import /etc/caddy/victorialogs-auth.conf
# 8190 is the loopback port compose publishes. It is not a container port
# and it is not open in the firewall.
reverse_proxy 127.0.0.1:8190
}
EOF
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
```
You should see: `Valid configuration` from validate, and no output at all from the reload.
If you do not: restore the copy with
`sudo cp /etc/caddy/Caddyfile.before-victorialogs /etc/caddy/Caddyfile`, reload, and read what
validate objected to. The usual cause is a `<DOMAIN>` left literal in the pasted block.
## 6. Firewall
```bash
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 443/udp
sudo ufw status verbose
```
You should see: `Status: active`, rules for 80/tcp, 443/tcp and 443/udp, and no rule mentioning
8190 or 9428.
If you do not: an inactive firewall means Prompt Zero did not finish; run `sudo ufw enable`. If
8190 appears, remove it with `sudo ufw delete allow 8190`, because Caddy reaches that port over
loopback and nothing else should. One thing to hold on to: every process on this box can reach
127.0.0.1:8190 with no password. That is what lets the log driver in step 7 write, and it is the
boundary of a single-tenant machine. If you ever share this box with someone whose access you
would not extend to your logs, that assumption stops holding and the loopback port becomes the
thing to close, not the hostname.
## 7. Start and verify
VictoriaLogs creates its storage on first start. No migration step, no wizard, no first account.
```bash
cd /srv/victorialogs
docker compose pull
docker compose up -d
for i in $(seq 1 30); do code=$(curl -sS -o /dev/null -w '%{http_code}' http://127.0.0.1:8190/health); echo "$i $code"; [ "$code" = 200 ] && break; sleep 5; done
printf '{"_msg":"caniselfhostit install check","service":"install-check"}\n' | curl -sS -o /dev/null -w '%{http_code}\n' -X POST -H 'Content-Type: application/stream+json' --data-binary @- 'http://127.0.0.1:8190/insert/jsonline?_stream_fields=service'
sleep 3
curl -sS http://127.0.0.1:8190/select/logsql/query -d 'query={service="install-check"}' | grep -c 'caniselfhostit install check'
curl -sS -o /dev/null -w '%{http_code}\n' https://<DOMAIN>/select/vmui/
curl -sS -o /dev/null -w '%{http_code}\n' -u "vlogs:$(sudo cat /srv/victorialogs/dashboard-password)" https://<DOMAIN>/select/vmui/
```
You should see, in order: the loop ending on `200`, which is `/health` answering `OK`; `200` from
the ingest call; `1` from the query, which is a line that went in over the JSON stream API coming
back out through LogsQL; `401` from the unauthenticated public call, which is the security check
in this step; and `200` from the authenticated one.
If you do not: a `502` with the container running means Caddy cannot reach 8190, so re-read
step 5. A `200` where you expected `401` means the `import` line did not load and your log store
is public to the internet, so fix that before anything else. A `0` from the query after a `200`
ingest means the insert was accepted but the `_stream_fields` name in the URL does not match the
JSON. For anything else, run `docker compose logs --tail 40 victorialogs`. A running container is
not success.
The browser tab is titled `UI for VictoriaLogs`. The query box is at the top, results underneath,
and the query language is LogsQL rather than SQL: `*` returns everything, a bare word matches the
message text, and `{service="install-check"}` filters on a stream field.
STOP: open https://<DOMAIN>/select/vmui/ in a private window, sign in as `vlogs` with the password from /srv/victorialogs/dashboard-password, and confirm the query `*` returns the `caniselfhostit install check` line. Do not continue until that line is on the screen.
Now ship real logs in. Docker's built-in `splunk` log driver writes to the Splunk HTTP Event
Collector paths VictoriaLogs answers, so nothing more has to be installed. List what is running
with `docker ps --format '{{.Names}}'`. If nothing but `victorialogs` comes back, keep the block
below for when you have a second service and go to step 8. Otherwise pick one of those services,
add this to it in its own compose file keeping the keys already there, and recreate that one
service with `docker compose up -d --force-recreate <service>`:
```yaml
logging:
driver: splunk
options:
splunk-url: "http://127.0.0.1:8190"
splunk-token: "PLACEHOLDER"
splunk-verify-connection: "false"
tag: "{{.Name}}"
```
The token is required by the driver and ignored by VictoriaLogs, which reads no token on that
path. The connection check is off on purpose: the driver probes the destination with an HTTP
OPTIONS request at container start and expects `200`, VictoriaLogs answers every OPTIONS request
with `204`, and a container whose probe fails does not start at all. Never put this block on the
`victorialogs` service itself. Then prove the loop:
```bash
before=$(curl -sS http://127.0.0.1:8190/select/logsql/query -d 'query=_time:10m' | wc -l)
sleep 20
after=$(curl -sS http://127.0.0.1:8190/select/logsql/query -d 'query=_time:10m' | wc -l)
echo "before=$before after=$after"
curl -sS http://127.0.0.1:8190/select/logsql/query -d 'query=_time:2m' | head -2
```
You should see: `after` larger than `before`, and one or two JSON lines carrying that service's
own output.
If you do not: equal numbers mean the driver is not delivering. Check the URL with
`docker inspect --format '{{.HostConfig.LogConfig}}' <container>`, and confirm the service really
was recreated rather than only restarted, because a logging change lands on recreate.
## 8. First backup and restore
One archive: the log partitions, the compose file, the Caddy password, the live Caddyfile and the
auth conf. The container stops for it, because a storage directory copied while partitions are
being merged is not a backup.
```bash
cd /srv/victorialogs
docker compose stop
sudo tar -czf /srv/victorialogs/backups/victorialogs-$(date +%F).tar.gz -C /srv/victorialogs data compose.yml dashboard-password -C /etc/caddy Caddyfile victorialogs-auth.conf
docker compose start
ls -lh /srv/victorialogs/backups/
```
You should see: one `.tar.gz` with a non-zero size. Downtime is a few seconds.
If you do not: a `Cannot open: No such file` for `victorialogs-auth.conf` means step 5 did not
finish, so go back. A zero-byte archive means `data` was empty, which is possible only if step 7
never ingested anything.
The only credential in that archive is `dashboard-password`, so treat the file as secret material.
A backup on the same disk is not a backup, so run this on your own machine, not the server:
```bash
mkdir -p ~/backups/victorialogs
scp vps:/srv/victorialogs/backups/*.tar.gz ~/backups/victorialogs/
```
To restore cold on a box that has been through Prompt Zero: `docker compose down`, recreate the
directories exactly as in step 2, untar the archive into /srv/victorialogs, move `Caddyfile` and
`victorialogs-auth.conf` back under /etc/caddy, `sudo systemctl reload caddy`, then
`docker compose up -d`, and re-run step 7's `401` and `200` checks before believing any of it.
`data/` is every log line you kept and `dashboard-password` is how you get back in, so a restore
missing the second is a lockout even when the first is intact.
## 9. Updating later
New versions are at https://github.com/VictoriaMetrics/VictoriaLogs/releases, with the changelog
at https://docs.victoriametrics.com/victorialogs/changelog/. Stay on the plain tag: `-enterprise`
and `-enterprise-fips` in the same Docker Hub repository are a separate commercial build under its
own licence rather than the Apache-2.0 one, and they expect a licence key. Take a backup, then
edit the image line in /srv/victorialogs/compose.yml to the new tag and its digest:
```bash
cd /srv/victorialogs
docker compose pull
docker compose up -d
docker compose logs --tail 30 victorialogs
```
You should see: the new version in the start-up log, then `started VictoriaLogs`.
If you do not: a container that exits immediately after an update almost always names a
command-line flag it no longer accepts, in the last line of that log. Roll back by putting the
previous tag and digest into compose.yml and running the same three commands. Re-run step 7's
health, ingest and query checks before calling the update done. Two numbers in `command:` are
worth revisiting at the same time: `-retentionPeriod=30d` decides how far back a query can reach,
and `-retention.maxDiskSpaceUsageBytes=5GiB` drops the oldest days when the disk fills first.
Raise either, then watch `du -sh /srv/victorialogs/data` for a week before trusting the figure.
## 10. What will probably go wrong
The install will look finished and the product will be empty. I had a green health check, a signed
certificate, a working login box, and a query screen answering every question with zero results,
and I spent twenty minutes assuming my query language was wrong. Nothing was wrong. Nothing was
shipping. A log store is the one service where a correct install and a useless one look identical
from outside, because the useful half is configuration on containers you are not installing today.
The other failure: a container refuses to start after you attach the log driver, saying it failed
to initialize the logging driver. That is the connection probe this page turns off. Leave it on,
or aim the driver where VictoriaLogs is not listening, and the container never runs.
## 11. Out of scope
- Do not add Grafana, Vector, vmauth, vmalert or Alertmanager. Upstream's demo runs all five;
this install is the log store alone.
- Do not set `-httpAuth.username` or `-httpAuth.password` on the container. Caddy is the door; a
second half-covering one only looks like protection.
- Do not open 8190 in the firewall or rebind it to 0.0.0.0.
- Do not configure alerting rules. They need vmalert plus a notifier, a second install.298 lines · 14,991 bytes
What this prompt will do
- Preflight
- Docker
- Layout
- Secrets
- compose.yml
- Nothing is public
- Start and verify
- First backup and restore
- Updating later
- What will probably go wrong
- Out of scope
Read out of the prompt’s own step headings at build time — if the prompt changes, this list changes with it.
You are Claude Code on the user's own computer. There is no server and no Prompt Zero:
everything in this prompt runs on this machine and stays on it.
Run every command on this computer, in the shell you are already in. Nothing in this prompt
uses ssh.
Install VictoriaLogs 1.52.0 under ~/selfhost/victorialogs, answering at http://localhost:8190.
## 1. Preflight
Say this before step 2 runs, because it decides whether the user wants this install at all. A log
store holds only what is shipped to it, and this one answers at http://localhost:8190, which means
this computer and nothing else. It collects what containers on this machine say. It cannot reach a
VPS, and while the machine sleeps it collects nothing.
Detect the OS and measure the machine:
```bash
uname -s
case "$(uname -s)" in
Darwin) vm_stat | awk '/page size/{p=$8} /free|inactive/{s+=$3} END {printf "%d MB available\n", s*p/1048576}' ;;
Linux) . /etc/os-release && echo "$ID $VERSION_CODENAME"; free -m | awk '/^Mem:/ {print $7 " MB available of " $2 " MB"}' ;;
MINGW*|MSYS*) powershell -Command "(Get-CimInstance Win32_OperatingSystem).FreePhysicalMemory" | awk '$1+0 {printf "%d MB available\n", $1/1024}' ;;
esac
df -h ~
```
`Darwin` is macOS, `Linux` is Linux, `MINGW` or `MSYS` is Windows under Git Bash. On Linux the
distribution ID and codename print next, for step 2. VictoriaLogs needs 1024 MB of RAM available
and 20 GB free on the home disk, and the image publishes amd64, arm64, arm/v7 and ppc64le. If
available RAM is under 1024 MB or free disk is under 20 GB, print both and stop. The floor is high
for one container because a log store fills disk: this one caps data at 5 GiB.
## 2. Docker
Check before installing anything:
```bash
docker info >/dev/null 2>&1 && echo "docker OK" || echo "docker MISSING"
docker compose version 2>/dev/null || true
```
If that printed `docker OK` and a compose version, skip to step 3.
Otherwise, install Docker for the OS step 1 detected:
- macOS: if `command -v brew` succeeds, run `brew install --cask docker`. If there is no
Homebrew, STOP: tell the user to download Docker Desktop from
https://www.docker.com/products/docker-desktop/ and install it, and wait until they
confirm. Either way, then STOP: tell the user to open Docker Desktop once, accept its
terms, and wait for the whale icon to say it is running. Do not continue until they
confirm.
- Windows: run `winget install -e --id Docker.DockerDesktop`. If winget is missing or the
install fails, STOP: tell the user to download Docker Desktop from the URL above and
install it, and wait until they confirm. Docker Desktop configures WSL 2 itself and may
ask for a reboot; if it does, STOP and tell the user to reboot and come back, this
prompt resumes at this step. Then STOP: have the user open Docker Desktop, accept its
terms, and confirm it says running.
- Linux, Debian or Ubuntu: install Docker Engine from download.docker.com's apt
repository, with its signing key saved to a file first, never piped into a shell. The
fence is guarded, a no-op on anything but a Linux with apt:
```bash
if [ "$(uname -s)" = "Linux" ] && command -v apt-get >/dev/null 2>&1; then
sudo apt-get update
sudo apt-get install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/$(. /etc/os-release && echo "$ID")/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/$(. /etc/os-release && echo "$ID") $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list >/dev/null
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
sudo usermod -aG docker "$USER"
fi
```
Adding the user to the docker group is root-equivalent on this machine; say that to the
user in one sentence, and tell them the group change lands at their next login.
- Linux, anything else: STOP. Tell the user to install Docker Engine and the compose
plugin with their distribution's package manager, and to run this prompt again once
`docker info` works.
Assert: `docker info` exits 0 and `docker compose version` prints a version. Do not
continue without both.
## 3. Layout
```bash
mkdir -p ~/selfhost/victorialogs/data ~/selfhost/victorialogs/backups
ls -la ~/selfhost/victorialogs
```
Assert: `data` and `backups` exist. `data` becomes `/vlogs` inside the container and holds every
ingested line in per-day partitions. There is no config directory: the flags in step 5 are the
whole configuration. No ownership fix is needed: the image declares no user, so the
process runs as root and writes world-readable partition directories, which lets step 8 archive
them without sudo.
## 4. Secrets
No secret is generated on this path and there is no `.env` file. The VPS path generates one
password because it puts a public hostname in front of a service with no login of its own; here
there is no hostname and no Caddy, so loopback is the whole door. Say plainly that anything on this
computer can read and write this log store with no credential, and the protection is that nothing
else reaches 8190.
## 5. compose.yml
```bash
cat > ~/selfhost/victorialogs/compose.yml <<'EOF'
# VictoriaLogs · the deterministic fallback for the local path. Authored by
# caniselfhostit from the upstream documentation, not copied from a repository:
# docker image ... https://docs.victoriametrics.com/victorialogs/quickstart/
# flags .......... https://docs.victoriametrics.com/victorialogs/
# log driver ..... https://docs.victoriametrics.com/victorialogs/data-ingestion/splunk/
#
# One service on the computer you are sitting at. The data path is relative to
# ~/selfhost/victorialogs/, so one file works on macOS, Linux and Windows, and
# stays a bind mount so the partition directories are visible in Finder or
# Explorer. The `command:` list is the whole configuration: no config file, no
# .env. No healthcheck, because the image is distroless. Plain v1.52.0 is the
# open source build; step 9 covers the -enterprise tags. Digest read from
# Docker Hub on 2026-08-14; amd64, arm64, arm/v7 and ppc64le.
#
# NOT YET VERIFIED: no harness run has been recorded against this file.
services:
victorialogs:
image: victoriametrics/victoria-logs:v1.52.0@sha256:47b820890d64c4575a2a0a46415dcd8a4fd59a0f1fcd6a377693d7aea639442e
container_name: victorialogs
restart: unless-stopped
command:
# Absolute, so the mount below is the only place logs can land.
- "-storageDataPath=/vlogs"
# Upstream's default retention is 7 days. Thirty is the choice here.
- "-retentionPeriod=30d"
# The ceiling that protects the disk: past this the oldest per-day
# partitions drop. A laptop disk fills sooner than a server one.
- "-retention.maxDiskSpaceUsageBytes=5GiB"
volumes:
# Every ingested line lands here, in per-day partition directories.
- ./data:/vlogs
ports:
# Loopback only: no other device on the wifi reaches 8190.
- "127.0.0.1:8190:9428"
EOF
cd ~/selfhost/victorialogs && docker compose config >/dev/null && echo "compose OK"
```
Assert: that prints `compose OK`. One service, one port, one bind mount.
## 6. Nothing is public
No reverse proxy, no certificate, no firewall rule, and each is a decision. There is no hostname to
resolve, a certificate attests a public name and nothing here has one, and nothing is published
beyond loopback. Browsers treat http://localhost as a secure context, so the query page works
without TLS.
8190 is bound to 127.0.0.1, this computer only: not the user's phone, not a laptop on the same
wifi, not anyone on the internet. For a log store that is a fair trade, because what it collects
lives here too. Confirm the binding:
```bash
grep -c '"127.0.0.1:' ~/selfhost/victorialogs/compose.yml
```
Assert: that prints `1`. A `0` means the port line was edited and the store may be listening on
every interface with no password in front of it; stop and fix the compose file.
## 7. Start and verify
VictoriaLogs creates its storage on first start. No migration step, no wizard, no first account.
```bash
cd ~/selfhost/victorialogs
docker compose pull
docker compose up -d
for i in $(seq 1 30); do code=$(curl -sS -o /dev/null -w '%{http_code}' http://localhost:8190/health); echo "$i $code"; [ "$code" = 200 ] && break; sleep 5; done
printf '{"_msg":"caniselfhostit install check","service":"install-check"}\n' | curl -sS -o /dev/null -w '%{http_code}\n' -X POST -H 'Content-Type: application/stream+json' --data-binary @- 'http://localhost:8190/insert/jsonline?_stream_fields=service'
sleep 3
curl -sS http://localhost:8190/select/logsql/query -d 'query={service="install-check"}' | grep -c 'caniselfhostit install check'
curl -sSL http://localhost:8190/select/vmui/ | grep -c 'UI for VictoriaLogs'
```
Assert all four and print what you received. The loop ends on `200`, `/health` answering `OK`. The
ingest call prints `200`. The query prints `1`: a line went in over the JSON stream API and came
back out through LogsQL, which is the whole product. The last prints more than `0`, because
`UI for VictoriaLogs` is the title the query page carries. If any of the four misses, stop, run
`docker compose logs --tail 40 victorialogs`, and name the cause. If `port is already allocated`
came back, find what holds 8190 (`lsof -nP -iTCP:8190 -sTCP:LISTEN`, `ss -ltnp | grep 8190` on
Linux, `netstat -ano | findstr :8190` on Windows) and stop until it is freed. A running container
is not success.
STOP: tell the user to open http://localhost:8190/select/vmui/, run the query `*`, and confirm they see the `caniselfhostit install check` line. Do not continue until they confirm.
Now ship real logs in. Docker's built-in `splunk` log driver writes to the Splunk HTTP Event
Collector paths VictoriaLogs answers, so nothing more installs. Run
`docker ps --format '{{.Names}}'`. If nothing but `victorialogs` is listed, print the block below,
say where it goes later, and go to step 8. Otherwise:
STOP: ask the user which service should ship its output, and wait. Do not continue until they confirm.
Add that block to the service they name, in its own compose file, then
`docker compose up -d --force-recreate <service>`:
```yaml
logging:
driver: splunk
options:
splunk-url: "http://127.0.0.1:8190"
splunk-token: "PLACEHOLDER"
splunk-verify-connection: "false"
tag: "{{.Name}}"
```
The token is required by the driver and ignored by VictoriaLogs, which reads no token there. The
connection check is off because the driver probes with an OPTIONS request at start-up expecting
`200`, VictoriaLogs answers OPTIONS with `204`, and a container whose probe fails never starts. Do
not put this block on `victorialogs` itself. Then prove the loop:
```bash
before=$(curl -sS http://localhost:8190/select/logsql/query -d 'query=_time:10m' | wc -l)
sleep 20
after=$(curl -sS http://localhost:8190/select/logsql/query -d 'query=_time:10m' | wc -l)
echo "before=$before after=$after"
curl -sS http://localhost:8190/select/logsql/query -d 'query=_time:2m' | head -2
```
Assert: `after` is greater than `before`, and the last command prints a line of that service's own
output. If the two are equal the driver is not delivering: check the URL with
`docker inspect --format '{{.HostConfig.LogConfig}}' <container>`.
## 8. First backup and restore
One archive: the log partitions and the compose file. The container stops for it, because a
storage directory copied mid-merge is not a backup. Downtime is a few seconds.
```bash
cd ~/selfhost/victorialogs
docker compose stop
tar -C ~/selfhost/victorialogs -czf ~/selfhost/victorialogs/backups/victorialogs-$(date +%F).tar.gz data compose.yml
docker compose start
ls -lh ~/selfhost/victorialogs/backups/
```
Assert: the archive exists and is non-empty. Print its size. There is no password file and no
`.env` here, so it holds logs and configuration, which is whatever those logs say.
That archive sits on the same disk as the data, which is not a backup, and on a laptop the disk and
the machine fail together. Ask the user for a destination that leaves this computer, a sync folder
or a USB stick, and copy it there with `cp`. In Git Bash a Windows drive is written `/d/Backups`,
not `D:\Backups`. Assert: the user confirms the filename is there. If they have nowhere, say that
this install has no backup.
To restore: `cd ~/selfhost/victorialogs`, `docker compose down`, `rm -rf data`, untar the archive
there, `docker compose up -d`, then re-run step 7's health and query asserts. On Linux the partition
files are owned by root, so that `rm -rf` needs `sudo`. The archive is the only copy of what the
containers that produced it have already rotated away.
## 9. Updating later
New versions are at https://github.com/VictoriaMetrics/VictoriaLogs/releases, changelog at
https://docs.victoriametrics.com/victorialogs/changelog/. Stay on the plain tag: `-enterprise` and
`-enterprise-fips` in the same Docker Hub repository are a separate commercial build under its own
licence rather than the Apache-2.0 one, and expect a licence key. Take a backup, then edit the
image line in compose.yml to the new tag and digest:
```bash
cd ~/selfhost/victorialogs
docker compose pull
docker compose up -d
docker compose logs --tail 30 victorialogs
```
Re-run step 7's checks first. Two numbers in `command:` are worth revisiting then:
`-retentionPeriod=30d` decides how far back a query reaches, and
`-retention.maxDiskSpaceUsageBytes=5GiB` drops the oldest days when the disk fills. Raise either,
then watch `du -sh ~/selfhost/victorialogs/data`.
## 10. What will probably go wrong
I closed the lid on a Friday and came back on Monday to a log store with nothing from the weekend.
Nothing was broken. The machine was asleep, the containers were not running, there was nothing to
collect, and the gap reads as a quiet weekend rather than an outage. The second thing is worse and
happens at the same moment: a container with the splunk log driver attached refuses to start if
VictoriaLogs is not up first, saying it failed to initialize the logging driver. Turn on Docker
Desktop's start-at-login, and after a reboot run `cd ~/selfhost/victorialogs && docker compose up -d`
before anything that ships to it.
## 11. Out of scope
- Do not expose this to the internet.
- Do not configure port forwarding on the router.
- Do not add a reverse proxy or TLS.
- Do not rebind 8190 to 0.0.0.0 so another machine can ship logs here. That publishes a readable
and writable log store, with no password, on every network this laptop joins.
- Do not add Grafana, Vector, vmauth, vmalert or Alertmanager, and do not configure alerting
rules: those need vmalert and a notifier, a second install.compose.local.ymlthe services, pinned · local layout35 lines
# VictoriaLogs · the deterministic fallback for the local path. Authored by
# caniselfhostit from the upstream documentation, not copied from a repository:
# docker image ... https://docs.victoriametrics.com/victorialogs/quickstart/
# flags .......... https://docs.victoriametrics.com/victorialogs/
# log driver ..... https://docs.victoriametrics.com/victorialogs/data-ingestion/splunk/
#
# One service on the computer you are sitting at. The data path is relative to
# ~/selfhost/victorialogs/, so one file works on macOS, Linux and Windows, and
# stays a bind mount so the partition directories are visible in Finder or
# Explorer. The `command:` list is the whole configuration: no config file, no
# .env. No healthcheck, because the image is distroless. Plain v1.52.0 is the
# open source build; step 9 covers the -enterprise tags. Digest read from
# Docker Hub on 2026-08-14; amd64, arm64, arm/v7 and ppc64le.
#
# NOT YET VERIFIED: no harness run has been recorded against this file.
services:
victorialogs:
image: victoriametrics/victoria-logs:v1.52.0@sha256:47b820890d64c4575a2a0a46415dcd8a4fd59a0f1fcd6a377693d7aea639442e
container_name: victorialogs
restart: unless-stopped
command:
# Absolute, so the mount below is the only place logs can land.
- "-storageDataPath=/vlogs"
# Upstream's default retention is 7 days. Thirty is the choice here.
- "-retentionPeriod=30d"
# The ceiling that protects the disk: past this the oldest per-day
# partitions drop. A laptop disk fills sooner than a server one.
- "-retention.maxDiskSpaceUsageBytes=5GiB"
volumes:
# Every ingested line lands here, in per-day partition directories.
- ./data:/vlogs
ports:
# Loopback only: no other device on the wifi reaches 8190.
- "127.0.0.1:8190:9428"agent-readable mirror: /self-host/axiom.md
The files, if you'd rather do it yourself
The cloud path with no agent involved: three files, in the order you'd use them. The cloud prompt above writes exactly these — if the two ever disagree, the files are the ones CI diffs. The local path ships its own compose file, collapsed under its own prompt.
compose.ymlthe services, pinned35 lines
# VictoriaLogs · the deterministic fallback. Authored by caniselfhostit from
# the upstream documentation, not copied from a repository:
# docker image ... https://docs.victoriametrics.com/victorialogs/quickstart/
# flags .......... https://docs.victoriametrics.com/victorialogs/
# log driver ..... https://docs.victoriametrics.com/victorialogs/data-ingestion/splunk/
#
# One service, configured entirely by the `command:` list: there is no
# configuration file and no .env. Upstream's own demo under deployment/docker
# runs seven services; this keeps the one that stores and answers queries. No
# healthcheck: the image is distroless, so every assert runs from the host, and
# no login screen, so Caddy basic_auth is the door. Plain v1.52.0 is the open
# source build; step 9 covers the -enterprise tags. Digest read from Docker Hub
# on 2026-08-14; amd64, arm64, arm/v7 and ppc64le.
#
# NOT YET VERIFIED: no harness run has been recorded against this file.
services:
victorialogs:
image: victoriametrics/victoria-logs:v1.52.0@sha256:47b820890d64c4575a2a0a46415dcd8a4fd59a0f1fcd6a377693d7aea639442e
container_name: victorialogs
restart: unless-stopped
command:
# Absolute, so the mount below is the only place logs can land.
- "-storageDataPath=/vlogs"
# Upstream's default retention is 7 days. Thirty is the choice here.
- "-retentionPeriod=30d"
# The ceiling that protects the disk: past this the oldest per-day
# partitions drop, whatever the retention period says.
- "-retention.maxDiskSpaceUsageBytes=5GiB"
volumes:
# Every ingested line lands here, in per-day partition directories.
- /srv/victorialogs/data:/vlogs
ports:
# Loopback only: Caddy from outside, the Docker log driver from inside.
- "127.0.0.1:8190:9428"Caddyfilethe hostname and TLS31 lines
# VictoriaLogs · the Caddy site block for this service.
#
# Authored by caniselfhostit from
# https://docs.victoriametrics.com/victorialogs/security-and-lb/
# https://caddyserver.com/docs/automatic-https and
# https://caddyserver.com/docs/caddyfile/directives/basic_auth
#
# Append this to /etc/caddy/Caddyfile, with <DOMAIN> replaced by the hostname
# pointed at this box. VictoriaLogs has no accounts and no sign-in form, and
# its one optional Basic Auth flag pair still leaves /health, /ping and
# /robots.txt open, so the door belongs here, where it covers every path.
# Needs Caddy 2.8 or newer.
<DOMAIN> {
encode zstd gzip
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options "nosniff"
X-Frame-Options "SAMEORIGIN"
Referrer-Policy "no-referrer"
-Server
}
# Credential lives in /etc/caddy/victorialogs-auth.conf (not published here).
import /etc/caddy/victorialogs-auth.conf
# 8190 is the loopback port compose publishes. It is not a container port
# and it is not open in the firewall.
reverse_proxy 127.0.0.1:8190
}install.shthe same install, no agent195 lines
#!/usr/bin/env bash
# VictoriaLogs · the agent-free install.
#
# Everything prompt.md tells an agent to do, as a script you can read first.
# Run it on the VPS, as a non-root user who is in the docker group:
#
# DOMAIN_HOST=logs.example.com ./install.sh
#
# Authored by caniselfhostit from the upstream documentation:
# https://docs.victoriametrics.com/victorialogs/quickstart/
# https://docs.victoriametrics.com/victorialogs/
# https://docs.victoriametrics.com/victorialogs/security-and-lb/
# https://docs.victoriametrics.com/victorialogs/data-ingestion/splunk/
# https://caddyserver.com/docs/caddyfile/directives/basic_auth
#
# One secret is generated: the Caddy basic_auth password. VictoriaLogs itself
# has no accounts, no sign-in form and no first-run wizard, so Caddy is the only
# door on the public hostname. Apache-2.0; the plain image tag is the open
# source build, and the -enterprise tags in the same Docker Hub repository are a
# separate commercial product this script does not use.
#
# This script installs the log store. It does not make anything ship logs to it;
# the summary at the end says where that block goes.
#
# NOT YET VERIFIED: no harness run has been recorded against this script.
set -euo pipefail
APP_DIR="${APP_DIR:-/srv/victorialogs}"
DOMAIN_HOST="${DOMAIN_HOST:-}"
die() { printf 'install.sh: %s\n' "$1" >&2; exit 1; }
# curl's own -w writes 000 on a failed connection; the || keeps set -e from
# ending the script before the assert below can name what it received.
http_code() {
local out
out="$(curl -sS -o /dev/null -w '%{http_code}' "$@")" || out="000"
printf '%s' "$out"
}
# --- 1. Refuse to start on a machine that is not ready -----------------------
[ -n "$DOMAIN_HOST" ] || die "set DOMAIN_HOST to the hostname you pointed at this server, e.g. logs.example.com"
command -v docker >/dev/null 2>&1 || die "docker is not installed. Run Prompt Zero first."
docker compose version >/dev/null 2>&1 || die "the docker compose plugin is missing"
command -v caddy >/dev/null 2>&1 || die "caddy is not installed on the host. Run Prompt Zero first."
command -v openssl >/dev/null 2>&1 || die "openssl is not installed"
caddy_ver="$(caddy version | head -1 | cut -d' ' -f1 | sed 's/^v//')"
caddy_major="${caddy_ver%%.*}"
caddy_rest="${caddy_ver#*.}"
caddy_minor="${caddy_rest%%.*}"
if [ "$caddy_major" -lt 2 ] || { [ "$caddy_major" -eq 2 ] && [ "$caddy_minor" -lt 8 ]; }; then
die "caddy ${caddy_ver} predates 2.8, where the basic_auth directive this install uses arrived"
fi
avail_mb="$(free -m | awk '/^Mem:/ {print $7}')"
[ "$avail_mb" -ge 1024 ] || die "only ${avail_mb} MB of RAM available; this install wants 1024 MB"
avail_gb="$(df -BG --output=avail /srv | tail -1 | tr -dc '0-9')"
[ "$avail_gb" -ge 20 ] || die "only ${avail_gb} GB free on /srv; a log store wants 20 GB"
resolved="$(getent hosts "$DOMAIN_HOST" | awk '{print $1; exit}')" || resolved=""
[ -n "$resolved" ] || die "$DOMAIN_HOST does not resolve yet. Add the A record, wait a minute, run this again."
# --- 2. Layout ---------------------------------------------------------------
#
# data/ becomes /vlogs inside the container and is the only thing it writes.
sudo install -d -m 750 -o "$(id -u)" -g "$(id -g)" \
"$APP_DIR" "$APP_DIR/backups" "$APP_DIR/data"
install -m 0644 "$(dirname "$0")/compose.yml" "$APP_DIR/compose.yml"
install -m 0644 "$(dirname "$0")/Caddyfile" "$APP_DIR/Caddyfile"
# --- 3. Password for Caddy basic_auth ----------------------------------------
if [ ! -f "$APP_DIR/dashboard-password" ]; then
umask 077
openssl rand -hex 24 > "$APP_DIR/dashboard-password"
chmod 600 "$APP_DIR/dashboard-password"
umask 022
fi
cd "$APP_DIR"
docker compose config >/dev/null
# --- 4. Caddy auth conf + site block -----------------------------------------
umask 077
caddy hash-password < "$APP_DIR/dashboard-password" > "$APP_DIR/auth.hash"
printf 'basic_auth {\n\tvlogs %s\n}\n' "$(cat "$APP_DIR/auth.hash")" > "$APP_DIR/auth.conf"
umask 022
sudo install -m 640 -o root -g caddy "$APP_DIR/auth.conf" /etc/caddy/victorialogs-auth.conf
rm -f "$APP_DIR/auth.hash" "$APP_DIR/auth.conf"
sudo grep -q basic_auth /etc/caddy/victorialogs-auth.conf \
|| die "the auth conf has no basic_auth block; publishing the site block now would expose an open log store"
if ! sudo grep -qF "$DOMAIN_HOST {" /etc/caddy/Caddyfile; then
sudo cp /etc/caddy/Caddyfile "/etc/caddy/Caddyfile.before-victorialogs"
printf '\n' | sudo tee -a /etc/caddy/Caddyfile >/dev/null
sed "s|<DOMAIN>|${DOMAIN_HOST}|g" "$APP_DIR/Caddyfile" | sudo tee -a /etc/caddy/Caddyfile >/dev/null
fi
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
# --- 5. Firewall -------------------------------------------------------------
if command -v ufw >/dev/null 2>&1; then
echo "==> 80/tcp and 443/tcp for Caddy, 443/udp for HTTP/3; 8190 stays closed"
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 443/udp
sudo ufw status verbose
fi
# --- 6. Start and assert -----------------------------------------------------
docker compose pull
docker compose up -d
echo "==> waiting for http://127.0.0.1:8190/health"
code=""
for _ in $(seq 1 30); do
code="$(http_code "http://127.0.0.1:8190/health")"
if [ "$code" = "200" ]; then break; fi
sleep 5
done
[ "$code" = "200" ] || die "loopback /health answered ${code}. Check: docker compose logs --tail 40 victorialogs"
ingest="$(printf '{"_msg":"caniselfhostit install check","service":"install-check"}\n' \
| curl -sS -o /dev/null -w '%{http_code}' -X POST -H 'Content-Type: application/stream+json' \
--data-binary @- 'http://127.0.0.1:8190/insert/jsonline?_stream_fields=service')" || ingest="000"
echo "==> jsonline ingest -> ${ingest}"
[ "$ingest" = "200" ] || die "the jsonline ingest endpoint answered ${ingest}, not 200"
sleep 3
found="$(curl -sS "http://127.0.0.1:8190/select/logsql/query" -d 'query={service="install-check"}' \
| grep -c 'caniselfhostit install check')" || found="0"
echo "==> query returned ${found} matching line(s)"
[ "$found" -ge 1 ] || die "the ingested line did not come back from /select/logsql/query; the insert was accepted but nothing is queryable"
unauth="$(http_code "https://${DOMAIN_HOST}/select/vmui/")"
echo "==> unauthenticated https://${DOMAIN_HOST}/select/vmui/ -> ${unauth}"
[ "$unauth" = "401" ] || die "unauthenticated public request returned ${unauth}, not 401. The log store is reachable without a password; fix Caddy before using this."
auth="$(http_code -u "vlogs:$(cat "$APP_DIR/dashboard-password")" "https://${DOMAIN_HOST}/select/vmui/")"
echo "==> authenticated https://${DOMAIN_HOST}/select/vmui/ -> ${auth}"
[ "$auth" = "200" ] || die "authenticated public request returned ${auth}, not 200"
# --- 7. First backup ---------------------------------------------------------
#
# Stopped on purpose: a storage directory copied while partitions are being
# merged is not a backup. Downtime is a few seconds.
STAMP="$(date +%Y%m%d-%H%M%S)"
docker compose stop
sudo tar -czf "$APP_DIR/backups/victorialogs-${STAMP}.tar.gz" \
-C "$APP_DIR" data compose.yml dashboard-password \
-C /etc/caddy Caddyfile victorialogs-auth.conf
docker compose start
ls -lh "$APP_DIR/backups/"
[ -s "$APP_DIR/backups/victorialogs-${STAMP}.tar.gz" ] || die "the backup archive is empty"
cat <<-DONE
VictoriaLogs is answering at https://${DOMAIN_HOST}/select/vmui/
1. Sign in as vlogs. Read the password with:
sudo cat ${APP_DIR}/dashboard-password
VictoriaLogs has no account of its own; Caddy is the door, and the
401 and 200 above are the proof it is shut and openable.
2. Nothing ships logs here yet. Add this to a service in its own compose
file, keep the keys already there, then recreate that one service with
docker compose up -d --force-recreate <service>:
logging:
driver: splunk
options:
splunk-url: "http://127.0.0.1:8190"
splunk-token: "PLACEHOLDER"
splunk-verify-connection: "false"
tag: "{{.Name}}"
The token is ignored by VictoriaLogs. The connection check is off
because the driver expects a 200 to an OPTIONS probe and VictoriaLogs
answers 204, which would stop the container from starting. Never put
this block on the victorialogs service itself.
3. Then confirm lines are landing:
curl -sS http://127.0.0.1:8190/select/logsql/query -d 'query=_time:5m'
4. Retention is 30 days with a 5 GiB ceiling on ${APP_DIR}/data, both set
in compose.yml. Watch: du -sh ${APP_DIR}/data
5. First backup at ${APP_DIR}/backups. It carries the password file, so
treat it as secret. Copy it off this disk tonight.
6. NOT YET VERIFIED on a clean harness machine.
DONEWhat you're signing up for
The part a vendor's comparison page leaves out. None of it is a reason not to do this; all of it is yours the moment you cancel Axiom.
- Logs only. No metrics, no traces, no application performance monitoring, no dashboards and no alert rules in this container. Grafana is the dashboard answer in this catalog and GlitchTip is the error-tracking one; the alerting Axiom sells needs vmalert plus a notifier, which is a separate install.
- There are no accounts. VictoriaLogs ships no sign-in form, no roles and no API tokens; its one optional Basic Auth flag pair still leaves /health, /ping and /robots.txt answering unauthenticated. This install therefore puts Caddy basic_auth in front of every path, generates the password on the server, and asserts a 401 before it reports success.
- The install is the easy half. A log store is only worth what is shipped to it, and shipping is configuration on the containers you already run. This entry hands you one working path, Docker's built-in splunk log driver pointed at the loopback port, plus a query that proves a line landed. Everything past that is yours to wire.
- You own retention and disk. Upstream's default is 7 days; this install sets 30 days with a 5 GiB ceiling on the data directory, and both numbers are yours to raise once you know your volume. Nobody sends a bill when the number is wrong, and nobody warns you either.
- Apache-2.0 for the single-node and cluster builds. The -enterprise and -enterprise-fips image tags in the same Docker Hub repository are a separate commercial product with its own licence and key; this entry pins the plain tag and never touches them.
Where this came from
“Both the single-node and cluster versions of VictoriaLogs are open source and free to use.”
- Upstream documents a single Docker image serving HTTP on port 9428, a built-in web UI for querying at /select/vmui, and a LogsQL query API at /select/logsql/query. source
- VictoriaLogs accepts logs via the Splunk HTTP Event Collector API, and upstream documents configuring Docker's built-in splunk log driver to send container output to it with no extra agent. source
- Upstream states that all VictoriaLogs components must run inside a protected trusted network and that requests from the internet must be authorized before being proxied to them. source
- Default retention is 7 days, changed with -retentionPeriod, and disk usage can be capped independently with -retention.maxDiskSpaceUsageBytes. source
- Caddy obtains and renews TLS certificates automatically for any public hostname named in the Caddyfile. source
Questions people actually ask
Answered from this page's own data — the same numbers, in sentences.
Can I self-host Axiom?
Not Axiom itself — the vendor does not ship a version you can run on your own server. What you can self-host is the job people pay it for, and the answer to that is VictoriaLogs. One container that ingests, stores and queries your logs, on a disk you own, with no per-gigabyte meter. The install is one command: one container behind Caddy with automatic TLS, secrets generated on the server rather than in a chat window, and a first backup taken before the agent says it is done, in about 10 minutes. The prompt on this page does it; the compose.yml, Caddyfile and install.sh below do the same install with no agent at all.
What replaces Axiom?
VictoriaLogs. One container that ingests, stores and queries your logs, on a disk you own, with no per-gigabyte meter. One container, one port, one directory: it ingests over the same Splunk, Elasticsearch, Loki and OpenTelemetry protocols your collectors already speak, stores logs in a columnar format on your own disk, and ships a query interface and query API of its own so nothing else has to be installed to read them. Docker's built-in splunk log driver points at it with four lines of compose, which makes the core loop reachable in one sitting. What it does not replace is the rest of the meter: there are no traces, no metrics, no dashboards and no alerting in the box, and the retention promise becomes a number you chose and a disk you watch. That is the trade, and it is why this is a one-command install with an ongoing habit attached. VictoriaLogs is Apache-2.0-licensed and free; nothing on this page is a hosted service we sell you.
What does self-hosting cost compared to Axiom?
1024 MB of RAM and 20 GB of disk — the smallest tier most VPS hosts sell, about $5 a month. VictoriaLogs itself is free and Apache-2.0-licensed; the bill is the server, plus a domain you probably already own. What you stop paying: Axiom Axiom Cloud, $25/mo — $300 a year, a metered rate, not a whole bill.
How hard is it really?
ONE COMMAND — under 10 minutes. The rule that produced that verdict: one container, no database, no outside integration, at most one secret. Nothing to negotiate with anyone else, nothing to back up separately, at most one secret to generate. This is the case where the compose file honestly is the whole install. The tier is derived from seven countable facts about the VictoriaLogs install, not from anyone's impression of it, and the whole rubric is published on the methodology page.
Can I run VictoriaLogs on my own computer instead of a server?
Yes — that is the second path in the prompt box above. "On my computer" installs the same VictoriaLogs on the machine you are sitting at: no VPS, no domain, no DNS, and nothing exposed to the internet. It checks for Docker first and installs Docker Desktop if the machine does not have it — macOS, Windows and Linux each get their own step — then binds everything to loopback, so the app answers on http://localhost and only on that computer. The catch: It runs happily on a laptop and collects the containers on that laptop, but a log store only holds what is shipped to it: a sleeping machine records nothing, and a VPS cannot reach a loopback port on your desk. Same discipline as the cloud path: pinned images, secrets generated on the machine, and a first backup taken before the prompt says it is done.
Content last checked 2026-08-14. Verdicts are derived from the published rubric on /methodology; corrections go through the issue tracker.