feat(docker): 13단계 운영 기반 — 백업 · 감시 · 관리자 단단히 · 휴지통 30일 · 메일 프로그램 자동 설정

전 직원 전환 전에 서버가 스스로 지키고 되살릴 수 있게 했다.
- 관리자 단단히(setup): 관리자 역할에서 impersonate 를 빼고 관리자 허용 IP 를 앱 네트워크로 — 관리자 비밀번호로 바깥 993 · JMAP 에서 직원 메일함에 대리 로그인되던 길을 막았다.
  운영에서 처음 걸 때는 되돌릴 복구 관리자가 있을 때만 건다(관리자 잠김 방지).
- 휴지통 · 스팸 30일 정리를 KST 03:00 으로 명시. imapsync 로 옮긴 메일도 옮긴 때부터 센다(임시 엔진 실측).
- restic 백업(호스트 systemd 03:30 — 메일 볼륨 warm → 엔진 멈춤 9초 안팎 → final, pg_dump, 업로드, env) · 복원(새 서버 차례를 빈 클론에서 실측) · 15분 점검 · 부팅 되살리기, Healthchecks 하트비트.
- 메일 프로그램 자동 설정: frontend nginx 의 autoconfig · autodiscover 고정 응답(엔진 것은 POP3 를 알리고 입력을 되돌려 쓴다).
- 모든 서비스 docker 로그 보존, DB 이미지 판 · digest 고정.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-02 13:44:44 +09:00
parent f0e7890bf1
commit 73babeaa2c
21 changed files with 2249 additions and 34 deletions
+28
View File
@@ -0,0 +1,28 @@
# arasystem 백업 (13단계 — mail/README.md "백업") — arasystem-backup.timer 가 매일 03:30 KST 에 부른다
# 설치: mail/README.md "백업" › "설치 (서버 B — 한 번)" (스크립트는 /opt/arasystem-ops 에 root 소유로 둔다 — 저장소 폴더를 바로 부르지 않는다)
[Unit]
Description=arasystem 백업 — 메일 엔진 · DB · 업로드 · env 를 restic 저장소로
# docker 가 없으면 스크립트가 스스로 실패해 알린다 — Requires 로 묶으면 docker 를 다시 시작할 때 이 서비스도 함께 멈춘다(엔진을 멈춘 채로)
Wants=docker.service network-online.target
After=docker.service network-online.target
[Service]
Type=oneshot
ExecStart=/bin/bash /opt/arasystem-ops/backup/arasystem-backup.sh
# 스크립트가 어떻게 끝나든(죽어도) 백업이 멈춘 메일 엔진이 남아 있으면 다시 띄운다
ExecStopPost=/bin/bash /opt/arasystem-ops/backup/arasystem-backup.sh ensure-mail-running
# 상태 파일 · DB 덤프 자리 (/var/lib/arasystem-ops — root 만)
StateDirectory=arasystem-ops
StateDirectoryMode=0700
# 처음 백업은 메일 전체를 올려 오래 걸릴 수 있다 (그 뒤로는 바뀐 것만 — 분 단위). 메일 엔진이 멈추는 final 은 스크립트가 따로 묶는다(BACKUP_FINAL_TIMEOUT)
TimeoutStartSec=6h
# 멈출 때(stop · 시간 초과) 스크립트가 정리할 시간 — 사본 지우기 → 엔진 띄우기(멈춤 끝내기 60초) → /fail 알림(재시도 포함 최대 1분 반) → 준비 180초
TimeoutStopSec=8min
# 스크립트 · docker CLI 에만 걸린다 — restic 컨테이너는 dockerd 아래라 물려받지 않는다(스크립트가 docker run --cpus 1 --cpu-shares 512 --memory 1g 로 묶는다)
Nice=10
IOSchedulingClass=best-effort
IOSchedulingPriority=7
PrivateTmp=true
NoNewPrivileges=true
ProtectSystem=full
ProtectHome=read-only
+13
View File
@@ -0,0 +1,13 @@
# 매일 03:30 KST — 엔진의 휴지통 · 스팸 정리(03:00)와 데이터 · 원문 정리(04:00 · 04:30 — setup 이 맞춘다) 사이
[Unit]
Description=arasystem 백업 — 매일 03:30 KST
[Timer]
OnCalendar=*-*-* 03:30:00 Asia/Seoul
# 서버가 꺼져 있어 놓쳤으면 켜진 뒤 바로 한 번 (메일 엔진이 몇 초 멈춘다). 그때 엔진은 아직 starting 이다 —
# 스크립트가 healthy 를 5분(BACKUP_HEALTHY_WAIT)까지 기다리고, 부팅 유닛(arasystem-mail-recover)이 먼저 돈다
Persistent=true
AccuracySec=1min
[Install]
WantedBy=timers.target
+16
View File
@@ -0,0 +1,16 @@
# arasystem 15분 점검 (13단계 — mail/README.md "감시") — arasystem-check.timer 가 부른다
[Unit]
Description=arasystem 점검 — 디스크 · 컨테이너 · 대기열 · 인증서 · 백업 · Spamhaus 키
# docker 가 없으면 점검이 그것을 문제로 알린다 — Requires 로 묶지 않는다
Wants=docker.service network-online.target
After=docker.service network-online.target
[Service]
Type=oneshot
ExecStart=/bin/bash /opt/arasystem-ops/monitor/arasystem-check.sh
TimeoutStartSec=5min
Nice=10
PrivateTmp=true
NoNewPrivileges=true
ProtectSystem=full
ProtectHome=read-only
+10
View File
@@ -0,0 +1,10 @@
# 15분마다 — 매시 07 · 22 · 37 · 52분 (백업이 시작하는 03:30 과 겹치지 않게 비켜 둔다)
[Unit]
Description=arasystem 점검 — 15분마다
[Timer]
OnCalendar=*-*-* *:07/15:00
AccuracySec=30s
[Install]
WantedBy=timers.target
@@ -0,0 +1,28 @@
# arasystem — 부팅 때 백업이 멈춘 메일 엔진을 다시 띄운다 (13단계 — mail/README.md "백업")
# 백업이 메일 엔진을 멈춘 몇 초 사이에 서버가 꺼지면 엔진은 멈춘 채 남는다 — docker stop 으로 멈춘 컨테이너는 restart: unless-stopped 여도
# 도커가 다시 띄우지 않는다. 표시 파일(/var/lib/arasystem-ops/mail-stopped-by-backup)이 있을 때만 띄우고, 떠 있는 것을 본 뒤 표시를 지운다
[Unit]
Description=arasystem — 백업이 멈춘 메일 엔진 다시 띄우기 (부팅 때)
Wants=docker.service
After=docker.service
# 부팅 뒤 따라잡는 백업(타이머의 Persistent)보다 먼저 — 백업이 이 엔진을 멈춘 채 받지 않게
Before=arasystem-backup.service
# 도커가 늦게 뜨면 실패한다 — 30초마다 다시, 10분 동안 10번까지
StartLimitIntervalSec=10min
StartLimitBurst=10
[Service]
Type=oneshot
ExecStart=/bin/bash /opt/arasystem-ops/backup/arasystem-backup.sh ensure-mail-running
Restart=on-failure
RestartSec=30
StateDirectory=arasystem-ops
StateDirectoryMode=0700
TimeoutStartSec=5min
PrivateTmp=true
NoNewPrivileges=true
ProtectSystem=full
ProtectHome=read-only
[Install]
WantedBy=multi-user.target