단일 VPS에서 효율적인 자가 호스팅 로그 관리 방법
단일 VPS 환경에서 journald와 logrotate를 활용한 기본 로그 관리부터 Loki, OpenSearch 도입 기준까지 정리했습니다. 메모리 사용량과 보존 정책을 고려하여 서버 자원을 최적화하고 효율적으로 로그를 검색하는 실무 가이드를 확인해 보시기 바랍니다.
단일 VPS에서 자가 호스팅 로그 관리의 실제 비용
단일 VPS(가상 사설 서버)에서의 자가 호스팅 로그 관리는 결국 하나의 질문으로 귀결됩니다. 검색 클러스터가 필요한가, 아니면 로그 로테이션과 grep만으로 충분한가 하는 점입니다. 대부분의 벤더 가이드는 로그 한 줄을 전송하기도 전에 노드 3개와 12 GB의 RAM을 요구하는 것부터 시작합니다. 단일 서버 환경에서는 이러한 답변이 무의미하므로, 아래 비교는 각 옵션이 데이터를 저장하기 전 소규모 서버에서 요구하는 자원을 기준으로 합니다.
서버를 한두 대 운영하면서 지난 화요일에 무슨 일이 있었는지 확인하려는 목적이라면, 이미 systemd-journald와 logrotate가 해당 역할을 수행하고 있으므로 다음 섹션 이후는 읽지 않아도 됩니다. 여러 대의 서버에서 로그를 한곳으로 모아 수주간의 데이터를 검색해야 한다면, Grafana Loki가 소규모 서버에 적합합니다. Loki는 로그 내용 전체가 아닌 레이블만 인덱싱하기 때문입니다. Elasticsearch와 OpenSearch는 실제 전체 텍스트 검색 기능을 제공하지만, 그 대가로 메모리 자원을 소모합니다. JVM(Java virtual machine) 힙 메모리에는 최소한으로 유지해야 하는 하한선이 존재하기 때문입니다.
journald로 시작하십시오. 대부분의 사람은 여기서 멈춥니다
systemd-journald는 현재 사용 중인 모든 Ubuntu 또는 Debian 서버에서 이미 실행 중입니다. 이 서비스는 모든 서비스 유닛의 표준 출력, 커널 메시지, 그리고 syslog로 전송된 모든 내용을 수집합니다. 네 가지 명령어로 대부분의 사고를 처리할 수 있습니다.
journalctl -u nginx.service --since "2026-08-14 09:00" --until "2026-08-14 10:00"
journalctl -p err -b
journalctl -f -u ssh.service
journalctl --disk-usage마지막 명령어는 Archived and active journals take up 1.1G in the file system.와 같은 줄을 출력합니다. 이 숫자가 추가 조치가 필요한지 결정하는 기준입니다. 수백 메가바이트 정도이고 -u와 --since 명령어로 필요한 정보를 찾을 수 있다면, 작업은 완료된 것입니다.
저널이 재부팅 후에도 유지되는지는 Storage= 설정과 /var/log/journal 디렉터리의 존재 여부에 달려 있습니다. 일반적인 Storage=auto 설정에서, journald는 해당 디렉터리가 존재하면 /var/log/journal에, 존재하지 않으면 /run/log/journal에 로그를 기록합니다. /run은 메모리 기반이므로, 해당 디렉터리가 없는 서버에서는 재부팅 시 모든 로그가 삭제됩니다. 이는 바로 로그를 확인해야 하는 시점에 로그가 사라지는 상황을 의미합니다. Ubuntu 이미지는 기본적으로 해당 디렉터리를 포함하고 있지만, 최소 설치 이미지나 컨테이너 기반 이미지는 그렇지 않은 경우가 많습니다.
ls -d /var/log/journal
sudo mkdir -p /var/log/journal
sudo systemctl restart systemd-journald
journalctl --disk-usage재시작 후, journalctl --disk-usage은 /run가 아닌 /var/log/journal 미만의 크기를 보고해야 합니다. 기본값은 이미 제한되어 있으며, 이것이 journald가 단순한 대안이 아닌 신뢰할 수 있는 도구인 주된 이유입니다. journald.conf 매뉴얼 페이지에 따르면 SystemMaxUse=은 파일 시스템 크기의 10%, SystemKeepFree=는 15%로 설정되며, 계산된 기본값은 각각 4G로 제한됩니다. SystemMaxFileSize=은 기본적으로 SystemMaxUse=의 8분의 1(최대 128M)로 설정되므로, 일반적으로 7개의 로테이션된 파일을 유지하게 됩니다. MaxRetentionSec=는 기본값이 0이며, 이는 시간 기반 삭제가 비활성화되어 있음을 의미합니다. 마지막 기본값을 다시 확인하십시오. 기본 설정 상태에서 저널은 시간 기준이 아닌 오직 크기에 의해서만 제한됩니다.
[Journal]
Storage=persistent
SystemMaxUse=2G
MaxRetentionSec=30day이 내용을 /etc/systemd/journald.conf.d/99-size.conf에 작성하고 journald를 재시작한 뒤, journalctl --disk-usage이 새로운 제한치에 맞춰 변경되었는지 확인하십시오. 다음 로테이션을 기다리지 않고 즉시 공간을 확보하려면 sudo journalctl --vacuum-size=500M 또는 sudo journalctl --vacuum-time=14d를 실행하십시오. 두 명령어 모두 삭제하는 모든 파일을 출력하므로, 아무런 출력도 없다면 삭제할 파일이 없다는 뜻입니다.
/var/log/nginx/access.log과 같이 저널 외부의 모든 로그는 logrotate의 작업이며, 이는 systemd 타이머를 통해 매일 실행됩니다. 한 가지 알아두어야 할 실패 사례가 있는데, 이는 df의 버그처럼 보일 수 있습니다. 로테이션이 발생하면 데몬이 여전히 파일을 열고 있는 동안 디렉터리 목록에서 이전 파일이 사라지기 때문에, df -h는 디스크가 가득 찼다고 보고하지만 du -sh /var/log은 훨씬 적은 사용량을 보고합니다. 공간은 프로세스가 로그 파일을 다시 열 때만 반환되며, 설정 파일의 postrotate reload 줄이 바로 이 역할을 합니다. sudo lsof -nP +L1는 삭제되었지만 여전히 열려 있는 파일을 나열하고, 해당 파일을 잡고 있는 프로세스 이름을 보여줍니다. 아무것도 변경하지 않고 규칙을 테스트하려면 sudo logrotate -d /etc/logrotate.d/nginx을 사용하십시오.
여러 서버의 로그를 하나의 수집기로 전송하기
서버가 한 대 이상이 되면 여러 대의 Linux 서버를 한 번에 관리하는 것이 로그를 한곳으로 모을 때 더 수월해집니다. 대부분의 배포판에는 rsyslog가 이미 설치되어 있으므로, 가장 저렴한 중앙 수집기 구성 방식은 각 송신 서버에서 파일 하나를 설정하는 것입니다.
*.* action(type="omfwd" target="logs.example.com" port="514" protocol="tcp")이 내용을 /etc/rsyslog.d/50-forward.conf로 저장하고 sudo rsyslogd -N1로 확인합니다. 이 명령은 설정을 검증하고 아무것도 시작하지 않은 채 종료됩니다. 그 후 rsyslog를 재시작합니다. 수집기 서버에서는 TCP 입력을 활성화합니다.
module(load="imtcp")
input(type="imtcp" port="514")기계적인 측면에서 두 가지 주의사항이 있습니다. 일반 syslog는 암호화나 인증을 제공하지 않으므로, 514 포트에 접근할 수 있는 대상이라면 무엇이든 실제 로그와 구분이 불가능한 로그 라인을 주입할 수 있습니다. 따라서 사설 네트워크나 VPN에 바인딩하고 방화벽으로 해당 포트를 차단해야 합니다. 두 번째로, 기본 작업 큐는 메모리에 상주합니다. 따라서 수집기에 도달할 수 없으면 큐가 가득 차고 메시지가 유실되며 사본도 남지 않습니다. rsyslog는 이러한 경우를 대비해 안정적인 전달 튜토리얼에서 디스크 보조 큐(disk assisted queue) 사용법을 안내하고 있습니다.
소규모 VPS에 ELK 스택이 적합하지 않은 이유
ELK는 저장 및 검색을 위한 Elasticsearch, 데이터 수집 파이프라인인 Logstash, 인터페이스인 Kibana를 의미합니다. 여기서 가장 큰 제약은 JVM 힙 메모리이며, 이는 로그가 도착하기 전에 미리 설정됩니다.
Elastic 공식 문서에 따르면, Elasticsearch 노드에 할당된 전체 메모리의 50%를 넘지 않도록 힙 크기를 설정해야 합니다. 해당 프로세스는 힙 외부 버퍼를 사용하며, 인덱스 파일을 빠르게 읽기 위해 운영체제의 파일 캐시에 의존하기 때문입니다. 따라서 2 GB의 힙을 사용하려면 최소 4 GB의 메모리를 갖춘 서버가 필요하며, 이는 Kibana나 서버의 본래 목적을 위한 애플리케이션 메모리를 제외한 수치입니다. 또한 Elastic은 노드의 역할과 전체 메모리 용량에 따라 힙 크기를 자동으로 조정하는데, 소규모 서버에서는 힙이 작게 설정되어 가비지 컬렉션(GC)에 대부분의 자원을 소모하게 됩니다.
Logstash는 소규모 서버의 자원 한계를 완전히 초과하는 요소입니다. Elastic의 JVM 설정 페이지에서는 일반적인 수집 작업을 위해 최소 4 GB에서 최대 8 GB의 힙을 권장합니다. 이는 4 GB VPS 전체를 파이프라인 중간의 단일 프로세스에 할당해야 함을 의미합니다.
The data behind this chart
[
{
"label": "Loki plus Alloy (no JVM)",
"documented_heap_mb": 0
},
{
"label": "OpenSearch demo compose",
"documented_heap_mb": 512
},
{
"label": "OpenSearch production example",
"documented_heap_mb": 2048
},
{
"label": "Logstash recommended minimum",
"documented_heap_mb": 4096
}
]위 수치들은 각 프로젝트의 공식 문서에서 권장하는 값입니다. 이는 테스트 환경에서 측정한 값이 아니며, 실제 워크로드에 따라 달라질 수 있습니다. OpenSearch의 샘플 compose 파일은 데모용으로 노드당 512 MB, 프로덕션 예시에서는 2048 MB를 설정하며, Logstash의 권장 하한선은 4096 MB입니다. Loki와 Alloy의 힙 항목이 0인 이유는 이들이 JVM 힙을 예약할 필요가 없는 Go 언어 기반 프로그램이기 때문입니다. 이것이 바로 수치 하나로 나타나는 결정적인 차이입니다. JVM 기반 구성 요소는 로그 유입 여부와 관계없이 예약된 메모리를 점유합니다.
그럼에도 불구하고 소규모 서버에서 Elastic 스택을 사용하고자 한다면, Logstash를 제거하고 가벼운 수집기를 사용하여 Elasticsearch로 직접 데이터를 전송하십시오. Logstash는 대규모 데이터의 파싱과 변환을 위해 존재하므로, 단일 서버 환경에서는 에지(edge)에서 처리하거나 해당 과정을 생략할 수 있습니다.
또한 Elasticsearch와 OpenSearch는 인덱스 파일을 메모리에 매핑하기 때문에 vm.max_map_count 값을 262144로 상향 조정해야 합니다. 리눅스의 기본 제한값은 이들에게 너무 낮기 때문입니다. 새로 구축한 서버에서 컨테이너가 시작 직후 종료된다면, 대부분 이 설정 문제일 가능성이 높습니다.
OpenSearch와 Elasticsearch: 무엇을 배포할 수 있는가?
라이선스 이력은 무엇을 실행할 수 있는지 결정하므로 짧게 설명합니다. 2021년 1월, Elastic은 Elasticsearch와 Kibana의 라이선스를 Apache 2.0에서 SSPL(Server Side Public License) 및 Elastic License 2.0 이중 모델로 변경했습니다. AWS는 마지막 Apache 2.0 코드를 포크하여 Apache 2.0을 유지하는 OpenSearch를 만들었습니다. 2024년 9월, Elastic은 무료 소스 코드의 또 다른 옵션으로 AGPLv3(GNU Affero General Public License version 3)를 추가했습니다. 1대의 VPS에서 직접 호스팅하는 개인 사용자에게는 이 모든 라이선스가 허용됩니다. 라이선스 제약은 해당 소프트웨어를 관리형 서비스로 타인에게 제공할 때 발생합니다.
소규모 서버에서 체감하는 차이는 역사적 배경보다 작습니다. 두 제품 모두 내부 엔진이 동일하기 때문입니다. 명칭은 다릅니다. 인덱스 생명주기 관리는 OpenSearch에서는 ISM(Index State Management), Elasticsearch에서는 ILM(Index Lifecycle Management)이라고 부릅니다. 2026년 8월 기준으로 OpenSearch 2.12 이상 버전은 최초 실행 시 관리자 암호를 설정하지 않으면 시작되지 않습니다.
sudo sysctl -w vm.max_map_count=262144
printf 'vm.max_map_count = 262144\n' | sudo tee /etc/sysctl.d/99-opensearch.conf
docker run -d -p 9200:9200 -p 9600:9600 -e "discovery.type=single-node" \
-e "OPENSEARCH_INITIAL_ADMIN_PASSWORD=<custom-admin-password>" \
opensearchproject/opensearch:latestsysctl -w 줄은 설정을 즉시 적용하며, /etc/sysctl.d/ 파일은 재부팅 후에도 설정이 유지되도록 합니다. curl -k -u admin:<password> https://localhost:9200 명령으로 컨테이너가 정상적으로 실행되었는지 확인합니다. 컨테이너는 데모 인증서를 사용하여 https로 응답하므로 -k 옵션으로 검증을 건너뜁니다. 정상적인 응답은 클러스터 이름과 버전이 포함된 작은 JSON 블록입니다. OpenSearch 설치 페이지는 Docker Desktop 사용자에게 호스트 메모리를 최소 4 GB 이상 할당할 것을 권장하며, 이는 해당 프로세스가 요구하는 자원 수준을 잘 보여줍니다.
Loki가 가벼운 이유: 전체 텍스트 인덱스 대신 레이블 사용
Loki는 레이블에 대한 인덱스 하나만 유지하고 로그 라인은 압축된 청크로 저장합니다. 쿼리는 먼저 스트림을 선택한 다음 텍스트를 필터링합니다. {unit="ssh.service"} |= "Failed password"는 레이블을 통해 스트림을 선택하고, 해당 청크에서 문자열을 검색합니다. 로그 라인의 본문은 인덱싱하지 않으므로 수집 비용이 저렴하며 메모리에 유지해야 할 역색인(inverted index)도 없습니다. 비용은 쿼리 시점에 발생하지만, 일반적으로 어떤 서비스를 확인하는지 알고 있는 상황에서는 합리적인 교환입니다.
Grafana 문서에 따르면 -target=all를 포함하여 Loki의 모든 기능을 하나의 프로세스에서 실행하는 모놀리식 모드는 하루 최대 약 20GB의 읽기 및 쓰기 볼륨까지 적합합니다. 일반적인 VPS 한 대는 이 범위 내에서 충분히 운영 가능합니다.
주의할 점은 레이블 카디널리티(cardinality)입니다. 레이블 값의 고유한 조합마다 하나의 스트림이 생성되며, 스트림 개수가 증가하면 Loki의 메모리 사용량과 인덱스 크기도 커집니다. 클라이언트 IP 주소나 요청 식별자를 레이블로 사용하면 값마다 스트림이 생성되므로, 트래픽이 많은 웹 서버는 하루에 수만 개의 스트림을 생성하여 프로세스가 커널에 의해 종료될 때까지 메모리를 점유할 수 있습니다. 레이블은 unit, host, job, level과 같이 종이에 적을 수 있을 정도로 개수가 제한적인 값으로 유지하십시오. 변동성이 큰 세부 정보는 로그 라인 자체에 포함하고, 쿼리 시점에 필터 표현식으로 검색하는 것이 좋습니다.
단일 VPS에 Loki와 Alloy 설치하기
두 개의 프로세스가 이 작업을 수행합니다. Loki는 로그를 저장하고 쿼리에 응답합니다. Grafana Alloy는 로그를 읽어 Loki로 전송합니다. 과거에는 Promtail이 전송기 역할을 했으나 2026년 3월 2일부로 지원이 종료되었습니다. 따라서 신규 설치 시에는 Alloy를 사용하며, Loki의 공식 Docker 예제도 현재 Alloy 설정을 포함하고 있습니다.
wget https://raw.githubusercontent.com/grafana/loki/v3.7.0/cmd/loki/loki-local-config.yaml -O loki-config.yaml해당 파일을 사용하기 전에 내용을 확인하십시오. 이 설정은 path_prefix: /tmp/loki을 /tmp/loki/chunks 하위의 청크로 지정하는데, 이는 데모용으로는 적합하지만 서버 운영에는 부적절합니다. 컨테이너의 /tmp 하위 경로는 컨테이너가 재생성될 때 유지되지 않으므로, 이미지 업데이트 시 기록이 모두 사라집니다. 반드시 마운트한 경로를 지정하십시오.
common:
instance_addr: 127.0.0.1
path_prefix: /loki
storage:
filesystem:
chunks_directory: /loki/chunks
rules_directory: /loki/rules
replication_factor: 1
ring:
kvstore:
store: inmemorydocker volume create loki-data
docker run --name loki -d \
-v $(pwd):/mnt/config -v loki-data:/loki \
-p 127.0.0.1:3100:3100 \
grafana/loki:3.7.0 -config.file=/mnt/config/loki-config.yaml
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:3100/ready마지막 명령은 200를 출력해야 합니다. Loki가 트래픽을 받을 준비가 되면 /ready이 HTTP 200을 반환하기 때문입니다. 다른 결과가 나온다면 프로세스가 시작 중이거나 설정이 거부된 것이며, docker logs loki을 통해 원인을 확인할 수 있습니다. 실행 명령의 두 가지 세부 사항은 의도된 것입니다. 포트는 127.0.0.1에만 게시됩니다. 샘플 설정에 auth_enabled: false이 포함되어 있는데 Loki 자체에는 사용자 인증 기능이 없으므로, 3100 포트에 접근할 수 있는 모든 사용자가 로그를 읽거나 가짜 로그를 쓸 수 있기 때문입니다. 루프백 주소에 유지하거나 VPN 또는 인증 기능을 갖춘 리버스 프록시 뒤에 배치하십시오. 명명된 볼륨(named volume)을 사용하는 이유는 이미지가 UID 10001인 loki 사용자로 실행되기 때문입니다. root 소유의 호스트 디렉터리를 바인드 마운트하면 컨테이너에서 쓰기 권한이 없습니다.
sudo apt-get update && sudo apt-get install -y gpg wget
sudo mkdir -p /etc/apt/keyrings/
sudo wget -O /etc/apt/keyrings/grafana.asc https://apt.grafana.com/gpg-full.key
echo "deb [signed-by=/etc/apt/keyrings/grafana.asc] https://apt.grafana.com stable main" \
| sudo tee /etc/apt/sources.list.d/grafana.list
sudo apt-get update
sudo apt-get install -y alloyAlloy는 /etc/alloy/config.alloy를 읽습니다. 이 설정은 시스템 저널과 특정 파일 세트를 읽어 로컬 Loki로 전송합니다.
loki.write "local" {
endpoint {
url = "http://127.0.0.1:3100/loki/api/v1/push"
}
}
loki.relabel "journal" {
forward_to = []
rule {
source_labels = ["__journal__systemd_unit"]
target_label = "unit"
}
}
loki.source.journal "read" {
forward_to = [loki.write.local.receiver]
relabel_rules = loki.relabel.journal.rules
labels = {job = "systemd-journal", host = "app-01"}
}
local.file_match "nginx" {
path_targets = [{"__path__" = "/var/log/nginx/*.log", "job" = "nginx", "host" = "app-01"}]
}
loki.source.file "nginx" {
targets = local.file_match.nginx.targets
forward_to = [loki.write.local.receiver]
}relabel 규칙은 저널 필드인 __journal__systemd_unit을 unit이라는 레이블로 복사합니다. 이것이 나중에 {unit="ssh.service"}이 정상적으로 작동하게 만드는 핵심입니다. 이 규칙이 없으면 유닛 이름이 로그 항목 내부에만 존재하게 되어 레이블로 선택할 수 없으며, 모든 쿼리마다 전체 데이터를 스캔해야 합니다.
sudo systemctl reload alloy
systemctl show -p User alloy
sudo journalctl -n 5
sudo -u alloy journalctl -n 5대부분의 설정이 여기서 막힙니다. Alloy는 root가 아닌 자체 서비스 계정으로 실행됩니다. 시스템 저널을 읽으려면 systemd-journal 그룹에 속해야 하며, Debian 및 Ubuntu의 /var/log/nginx 하위 파일들은 adm 그룹에 속합니다. systemctl show가 마지막 명령에서 출력한 계정으로 대체하십시오. 만약 root로 실행했을 때보다 훨씬 적은 항목만 반환된다면, 해당 계정이 시스템 저널을 읽지 못하는 것이며 설정이 아무리 정확해도 Loki는 비어 있게 됩니다. 필요한 그룹을 추가한 뒤 sudo usermod -aG systemd-journal,adm alloy에 이어 sudo systemctl restart alloy를 실행하여 재시작하십시오.
curl -G -s "http://127.0.0.1:3100/loki/api/v1/query_range" \
--data-urlencode 'query={job="systemd-journal"}' \
--data-urlencode 'limit=5' | jq '.data.result | length'0보다 큰 숫자는 해당 레이블을 가진 스트림이 존재하며 항목을 보유하고 있음을 의미합니다. 0은 해당 레이블로 아직 데이터가 도착하지 않았음을 뜻합니다. 한 가지 기본 설정이 흔한 오해를 설명합니다. loki.source.journal는 max_age을 7h로 설정하므로, 새로 시작하면 최근 7시간 분량의 저널만 읽고 그 이전 데이터는 읽지 않습니다. 사용자 인터페이스가 필요하다면 동일한 서버에 Grafana를 실행하고 Loki 데이터 소스를 http://127.0.0.1:3100.로 지정하십시오. 컨테이너 로그는 다른 소스가 필요합니다. Alloy는 실행 중인 Docker 컨테이너를 탐색하여 로그를 테일링(tailing)하는데, 이는 Loki의 시작하기 예제에서 수행하는 방식과 동일합니다. VPS의 단일 노드 k3s 클러스터 환경에서는 해당 작업이 kubelet이 기록하는 파드 로그 디렉터리로 이동합니다.
보존 기간: 로그가 삭제되는 시점 결정하기
디스크가 가득 찰 때까지 보존 기간을 정하지 않다가, 서비스가 중단된 새벽 3시에 급하게 결정하는 경우가 많습니다. 운영 첫날 다음 두 가지 질문을 통해 보존 기간을 결정하십시오. 실제로 과거 데이터를 얼마나 자주 조회하는지, 그리고 다음 달 사고 검토 시 어떤 데이터가 반드시 필요한지 확인해야 합니다. 단일 서버 환경이라면 14일에서 30일 정도면 두 질문에 대한 답이 됩니다.
Loki는 compactor를 활성화하기 전까지는 데이터를 전혀 삭제하지 않습니다. 보존 설정은 기본적으로 꺼져 있으며, 설정 파일에 retention_period 항목을 두고도 아무런 동작을 하지 않아 볼륨이 가득 차는 상황을 겪는 사용자가 많습니다.
limits_config:
retention_period: 744h
compactor:
working_directory: /loki/retention
compaction_interval: 10m
retention_enabled: true
retention_delete_delay: 2h
retention_delete_worker_count: 150
delete_request_store: filesystem744h는 31일입니다. 이 블록을 관리하는 네 가지 규칙은 다음과 같습니다.
- 보존 정책은 compactor에 의해 적용되며, Grafana 문서에서는 compactor를 단일 인스턴스로 실행할 것을 권장합니다. 단일 VPS 환경에서는 자동으로 그렇게 동작합니다.
- 최소 보존 기간은 24h이며, 보존 기능은 인덱스 기간이 24h일 때만 작동합니다. 예제
schema_config은 이미period: 24h를 사용하고 있으므로 그대로 두십시오. retention_enabled이 true로 설정되면delete_request_store설정이 필수입니다. 이는 삭제 요청을 저장하는 저장소를 지정하므로, 파일 시스템 기반의 단일 노드 환경에서는 스키마에 정의된object_store: filesystem와 일치시켜야 합니다.- 청크는 먼저 삭제 대상으로 표시된 후
retention_delete_delay(여기서는 2h)이 지나야 제거되므로, 실제 여유 공간은 정책에 명시된 시간보다 늦게 확보됩니다. 설정을 변경하고 5분 뒤에df를 확인하여 정책 적용 여부를 판단하지 마십시오.
OpenSearch는 개별 로그 라인이 아닌 인덱스 전체를 삭제하므로 로그 인덱스를 일 단위로 생성합니다. ISM 정책은 인덱스를 여러 상태로 전환하다가 설정된 기간이 지나면 삭제하며, ism_template를 사용하면 새로운 인덱스에 정책이 자동으로 적용되므로 수동으로 관리할 필요가 없습니다.
14일 후 로그 인덱스를 삭제하는 ISM 정책
{
"policy": {
"description": "delete log indexes after 14 days",
"default_state": "hot",
"states": [
{
"name": "hot",
"actions": [],
"transitions": [
{ "state_name": "delete", "conditions": { "min_index_age": "14d" } }
]
},
{
"name": "delete",
"actions": [ { "delete": {} } ],
"transitions": []
}
],
"ism_template": { "index_patterns": ["logs-*"], "priority": 100 }
}
}_plugins/_ism/policies/logs-retention에 PUT 요청을 보내 정책을 생성하십시오. 템플릿은 정책 생성 이후에 만들어진 인덱스에만 적용되므로, 이미 디스크에 존재하는 인덱스는 수동으로 정책을 연결해야 합니다.
어떤 시스템을 사용하든 보존 정책은 이를 뒷받침하는 여유 공간 확인 절차가 있어야 의미가 있습니다. 10일 치 로그만으로도 볼륨이 가득 찬다면 14일 보존 정책은 아무런 도움이 되지 않습니다. 따라서 정책과 함께 VPS 디스크 상태 모니터링을 설정하고, 사용률이 80%에 도달하면 알림을 받도록 구성하십시오.
GB당 필요한 디스크 용량
정직한 답변은 로그의 라인 수와 필드 구성에 따라 달라지므로, 공개된 비율을 맹신하기보다 실제 데이터로 직접 측정해야 합니다. 각 시스템의 메커니즘은 예측 방향이 다를 정도로 차이가 큽니다. OpenSearch와 Elasticsearch는 저장된 문서와 함께 모든 인덱싱된 필드에 대해 역색인(inverted index)을 생성하므로, 디스크에 기록되는 데이터는 원본 텍스트보다 크며 복제본(replica) 개수만큼 용량이 배가됩니다. 단일 노드 환경에서는 복제본 개수를 0으로 설정하십시오. 동일 노드에 있는 복제 샤드는 해당 노드 장애 시 함께 소실되기 때문입니다. 이를 1로 두면 디스크 사용량이 2배가 되며 클러스터 상태는 영구적으로 yellow로 고정됩니다. 반면 Loki는 압축된 청크와 작은 레이블 인덱스를 기록하므로, 디스크 점유율은 로그 라인의 압축된 크기를 따라갑니다.
sudo du -sh /var/lib/docker/volumes/loki-data/_data
curl -k -u admin:<password> "https://localhost:9200/_cat/indices?v&h=index,docs.count,store.size"이틀 연속으로 해당 시스템을 실행하십시오. 그 차이가 일일 증가량입니다. 이 수치에 보존 기간(일 단위)을 곱하고, 압축 및 병합을 위한 여유 공간 약 30%를 더한 뒤 볼륨 크기와 비교하십시오. 만약 용량이 부족하다면 디스크를 구매하기 전에 보존 기간을 줄이십시오. 더 큰 볼륨을 사용하는 것은 동일한 문제를 몇 주 뒤로 미루는 것에 불과합니다.
소형 서버에서 가장 먼저 발생하는 장애
메모리가 가장 먼저 부족해집니다. 커널의 OOM(out of memory) killer는 큰 프로세스를 선택하는데, 로깅 서버에서 가장 큰 프로세스는 보통 JVM입니다. journalctl -k | grep -i "killed process"은 괄호 안에 프로세스 이름을 표시하며 종료된 프로세스를 보여줍니다. 희생자가 항상 로그 스택인 것은 아닙니다. sshd나 데이터베이스가 선택될 수도 있으며, 이것이 바로 로깅 실험이 정작 로그를 수집하려던 애플리케이션을 중단시키는 방식입니다. 컨테이너에 명시적인 상한선을 설정하여 장애가 의도한 곳에서 발생하도록 해야 하며, 이것이 바로 Docker Compose의 메모리 제한이 존재하는 이유입니다.
디스크가 두 번째로 문제를 일으키며, 검색 엔진은 특정하고 식별 가능한 방식으로 실패합니다. Elasticsearch와 OpenSearch는 여러 단계에서 디스크 사용량을 감시합니다. 하위 워터마크는 85%, 상위 워터마크는 90%에 설정되어 있습니다. 95%의 플러드 단계에 도달하면 해당 노드에 샤드가 있는 모든 인덱스는 index.blocks.read_only_allow_delete 블록을 받게 되며, 이후 쓰기 작업은 blocked by: [FORBIDDEN/12/index read-only / allow delete (api)]와 함께 실패합니다. 사용량이 상위 워터마크 아래로 떨어지면 블록은 해제됩니다. 먼저 여유 공간을 확보하고, 블록이 계속 유지될 경우에만 수동으로 해제하십시오.
curl -k -u admin:<password> -X PUT "https://localhost:9200/_all/_settings" \
-H 'Content-Type: application/json' \
-d '{"index.blocks.read_only_allow_delete": null}'Loki는 더 조용하게 실패합니다. 전환할 수 있는 읽기 전용 모드가 없기 때문에 볼륨이 가득 차면 전송 측에서 푸시 실패가 발생하고 쿼리 결과에 공백이 생깁니다. 카디널리티(cardinality) 문제는 오류가 아닌 메모리 사용량의 완만한 상승으로 나타납니다. 사고가 발생한 뒤가 아니라 정기적으로 chunks 디렉터리의 크기를 확인하십시오.
마지막 장애는 잘못된 데이터를 입력하는 것입니다. 로그 시스템은 메트릭 시스템이 아닙니다. 10초마다 샘플링하여 텍스트로 저장하는 CPU 부하 데이터는 보관 비용이 많이 들고 그래프로 표현하기에도 부적절합니다. 해당 작업은 Ubuntu 24.04의 Zabbix 모니터링 서버와 같은 도구의 역할입니다. 애플리케이션 예외는 그룹화, 중복 제거, 스택 트레이스 보기가 필요하며, 이는 자체 호스팅 오류 추적기의 역할입니다. 사이트가 다운되었는지 확인하는 작업은 별개의 영역이며, Uptime Kuma와 같은 가동 시간 및 상태 페이지를 통해 해결할 수 있습니다. 로그 시스템은 사람이 읽을 텍스트 라인을 저장하는 용도로만 사용하십시오.
FAQ
Do I need Elasticsearch to search my server logs?
Not for one or two servers. journalctl already filters by unit, priority, boot and time range, and rotated files answer to grep and zgrep. A search cluster earns its memory when you have many machines, when you need free text search across all of them at once, or when several people need a shared interface. Below that, journald with a size limit and a retention time does the same job for no extra RAM.
How much RAM do I need for self-hosted log management?
Use each project's published figures rather than a rule of thumb. Loki and Alloy are Go programs with no heap to reserve up front, and Grafana documents monolithic Loki at up to approximately 20GB per day. OpenSearch's sample compose sets 512 MB of heap for a demo and 2 GB in its production example, and Elastic says heap must stay at or below 50% of total memory, so a 2 GB heap means a 4 GB machine before Kibana. Logstash's documentation recommends no less than 4GB of heap on its own. Those are documented settings, not benchmarks, so measure your own load before sizing a plan.
What is the real difference between Loki and OpenSearch for logs?
The index model. Loki indexes labels only and keeps the log body as compressed chunks that are scanned at query time, so writes are cheap and queries cost more when they are broad. OpenSearch indexes the content of the fields, so arbitrary full text search is fast and both memory and disk pay for the index. Choose Loki when you know which service and time window you want. Choose OpenSearch when you need to search text you cannot predict in advance.
How long should I keep logs on a VPS?
Pick the number before the disk picks it for you. Set it in exactly one place per system: MaxRetentionSec= and SystemMaxUse= for journald, retention_period with the compactor enabled for Loki, and an ISM policy with min_index_age for OpenSearch. For most single server setups, 14 to 30 days covers debugging and incident review. Anything you must keep longer belongs in a copy stored off the box, because a log kept only on the server that failed is not a record.
Is Promtail still the way to ship logs to Loki?
No. Promtail reached end of life on 2 March 2026, and Grafana Alloy replaces it. Loki's own Docker install example now ships an Alloy configuration, and Grafana provides a converter that turns an existing Promtail config into Alloy syntax. An existing Promtail install keeps running, but it receives no fixes, so treat migration as maintenance rather than an upgrade you can postpone indefinitely.