약 13분 읽기
서버 한 대에 21개를 올리게 된 과정
처음에는 단순한 파일 서버를 운영하는 것이 목표였다. 그러나 점차 다양한 요구 사항이 생기면서 여러 가지 애플리케이션을 동시에 운영할 필요가 있었다. 이때 Docker라는 도구를 알게 되었고, 하나의 서버에서 여러 개의 애플리케이션을 빠르게 관리할 수 있게 되었다. Docker를 활용하면서 별도의 물리적 서버를 추가하지 않아도 된다는 점이 매력적이었다.
다양한 애플리케이션을 운영하면서 가장 큰 도전은 성능과 안정성을 유지하는 것이었다. 여러 컨테이너를 동시에 실행하다 보면 자원 관리가 중요해지기 마련이다. 각 컨테이너가 적절한 자원을 할당받도록 하고, 서버 전체의 성능을 모니터링하는 과정이 필요했다. 또한, 애플리케이션 간의 네트워크 설정과 데이터베이스 연결 등도 신경 써야 할 부분이었다.
애플리케이션을 점점 더 추가하면서 단순히 컨테이너를 늘리기만 해서는 안 되는 상황이 되었다. 각각의 서비스가 독립적으로 동작하면서도 서로 잘 연동되도록 설계하는 데에 신경을 써야 했다. 특히 데이터베이스를 공유하는 서비스들은 보안과 권한 관리가 필수적이었다. 이런 상황에서 Docker 네트워크와 볼륨 기능이 많은 도움이 되었다.
이렇게 해서 최종적으로 21개의 Docker 컨테이너를 하나의 서버에 올리게 되었다. 다양한 요구 사항에 맞춰 여러 가지 애플리케이션을 운영하면서도 일정한 수준의 성능과 안정성을 유지할 수 있었다. 물론, 중간중간 시행착오를 겪기도 했지만, 이렇게 해서 많은 것을 배울 수 있었다.
shared-db 네트워크로 DB 하나만 쓰기
처음에는 각 애플리케이션마다 별도의 DB 인스턴스를 운영하려 했지만, 리소스가 낭비되어 더 나은 방법이 필요했다. MariaDB 하나를 여러 컨테이너가 공유하도록 구성하면서도 각 애플리케이션 데이터의 격리를 유지할 방법을 찾았다. 이를 위해 Docker의 external 네트워크 기능을 활용해 ‘shared-db’라는 전용 네트워크를 만들었다. 이 네트워크에 DB가 필요한 애플리케이션만 연결하고, 각 서비스마다 별도의 데이터베이스와 유저를 설정했다.
WordPress 컨테이너 설정에서 중요한 포인트는 모든 컨테이너의 DB 연결을 ‘shared-db’ 네트워크를 경유하도록 한 점이다.
services:
wordpress:
image: wordpress:latest
container_name: wordpress-autoflow
restart: always
ports:
- '127.0.0.1:8080:80' # 외부 직접 노출 차단, nginx만 앞에 둔다
environment:
WORDPRESS_DB_HOST: mariadb:3306
WORDPRESS_DB_USER: wp_autoflow
WORDPRESS_DB_PASSWORD: '${WP_DB_PASSWORD}'
WORDPRESS_DB_NAME: wordpress_autoflow
WORDPRESS_TABLE_PREFIX: wp_
volumes:
- wordpress-data:/var/www/html
- ./php-tuning.ini:/usr/local/etc/php/conf.d/wordpress-tuning.ini:ro
networks:
- shared-db
volumes:
wordpress-data:
networks:
shared-db:
external: true # 미리 만들어둔 공용 네트워크를 가져다 쓴다
shared-db 네트워크에 붙는 WordPress 컨테이너 (docker-compose.yml)
이렇게 구성함으로써 DB 인스턴스를 공유하면서도 데이터의 무결성과 보안을 유지할 수 있었다. 각 서비스가 별도의 네트워크에 접속하도록 설정했기 때문에 MariaDB의 리소스를 최적화하면서도 불필요한 성능 저하를 방지할 수 있었다. 결과적으로 서버 자원을 빠르게 활용하고, 관리 역시 간소화하는 데 큰 도움이 되었다.
포트를 127.0.0.1에 묶은 이유
초기 서버 설정에서는 모든 포트를 0.0.0.0으로 열어두었다. 그러나 로그를 살펴보던 중 낯선 IP 주소로부터의 로그인 시도가 있는 것을 발견했다. 이는 보안에 심각한 위협이 될 수 있으므로 이를 바로잡을 필요가 있었다. 따라서 외부로부터의 직접적인 접근을 차단하기 위해 포트를 127.0.0.1에 묶기로 결정했다. 이렇게 함으로써 보안 취약점을 줄이고, 외부 서비스가 서버 내부의 리소스에 접근하는 경로를 제한할 수 있었다.
이런 설정은 서버를 외부로부터 안전하게 보호하는 데 큰 도움이 된다. 물론, 외부 서비스와의 통신이 필요한 경우도 있다. 이럴 때는 nginx 리버스 프록시를 사용해 80과 443 포트만 외부에 노출했다. nginx는 요청을 적절하게 라우팅하면서도 보안 계층을 추가로 제공한다. 결과적으로, 외부 접근은 제한하되 필요한 서비스는 원활히 운영할 수 있는 환경을 구축할 수 있었다.
127.0.0.1로 포트를 묶는 방식은 서버 자원 관리에도 유리했다. 외부 접근이 제한되면서 불필요한 외부 요청으로 인한 리소스 소모를 줄일 수 있었다. 동시에 서버 내부에서 발생하는 네트워크 트래픽만 처리하면 되므로, 성능 최적화에도 도움이 되었다. 이러한 설정은 특히 여러 서비스가 동시에 운영될 때, 각각의 자원을 손 안 가게 분배하는 데 중요한 역할을 했다.
n8n 워크플로우 20개를 날린 날
n8n을 처음 시작할 때는 간단히 SQLite를 사용했다. 당시에는 별 문제가 없을 것이라 생각했지만, 워크플로우가 20개를 넘어서면서 상황이 급변했다. 갑작스럽게 모든 워크플로우가 사라져버린 것이다. 복구하려 했지만, 이미 저장된 데이터는 손 쓸 수 없을 만큼 엉망이 되어 있었다. 이후 안정성을 위해 PostgreSQL로 옮기기로 결심했다.
PostgreSQL로의 이전은 또 다른 문제를 야기했다. 초기 설정에서 간과했던 부분이 있었는데, 바로 컨테이너 간의 의존성 문제였다. 특히 n8n이 postgres보다 먼저 실행되면서 제대로 동작하지 않는 상황이 빈번했다. 이로 인해 서비스가 계속 죽어버리는 불상사가 발생했다.
services:
postgres:
image: postgres:16-alpine
container_name: n8n-postgres
restart: always
environment:
- POSTGRES_DB=n8n
- POSTGRES_USER=n8n
- POSTGRES_PASSWORD=${N8N_PG_PASSWORD}
volumes:
- n8n_postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U n8n -d n8n"]
interval: 5s
timeout: 5s
retries: 10
n8n:
image: n8nio/n8n:latest
container_name: n8n
restart: always
ports:
- "127.0.0.1:5678:5678"
depends_on:
postgres:
condition: service_healthy # 이 줄이 없으면 n8n이 먼저 떠서 죽는다
environment:
- N8N_HOST=n8n.example.com
- N8N_PROTOCOL=https
- WEBHOOK_URL=https://n8n.example.com/
healthcheck 없이 depends_on만 걸면 죽는다 (n8n + postgres)
depends_on만 설정했을 경우, postgres가 완전히 시작되기 전에 n8n이 연결을 시도하게 된다. 이로 인해 n8n이 데이터베이스에 접근할 수 없어 오류가 발생하며, 재시작을 거듭하는 상황이 반복되었다. 문제를 해결하기 위해 service_healthy 조건을 추가해 postgres가 완전히 준비된 이후에 n8n이 실행되도록 조정했다. 이 설정 변경 이후로는 안정적으로 컨테이너들이 기동되었다.
USB 동글이 재부팅마다 사라지는 문제
처음에는 Zigbee2mqtt를 사용하기 위해 USB 동글을 컨테이너에 넘기는 과정에서 문제가 발생했다. 컨테이너 재시작 시 USB 동글의 경로가 /dev/ttyUSB1로 밀리는 일이 잦았다. 그 결과 Zigbee 장치들이 제대로 인식되지 않아 서비스를 사용할 수 없었다. 이 문제를 해결하고자 초기에는 고정 경로를 설정하는 방법을 고민했다. 그러나 USB 장치의 식별자가 매번 바뀌는 상황이라 by-id 경로를 사용하여 장치를 고정적으로 참조하도록 설계를 변경했다.
이 같은 상황에서
services:
zigbee2mqtt:
image: koenkk/zigbee2mqtt:latest
container_name: zigbee2mqtt
restart: unless-stopped
network_mode: host # mDNS 검색 때문에 host 모드가 필요하다
volumes:
- ./data:/app/data
- /run/udev:/run/udev:ro
devices:
# ttyUSB0로 두면 재부팅 때 번호가 밀린다 → by-id 고정 경로 사용
- /dev/serial/by-id/usb-ITead_Sonoff_Zigbee_3.0_USB_Dongle-if00:/dev/ttyACM0
environment:
- TZ=Asia/Seoul
USB 동글을 컨테이너에 넘기기 (zigbee2mqtt)
by-id 경로를 사용함으로써 장치의 식별자가 변해도 컨테이너 내에서 항상 같은 경로로 인식되도록 할 수 있었다. 장치 경로가 고정되면서 Zigbee 장치들이 정상적으로 인식되었고, 경로 불일치로 인한 문제는 사라졌다. 이러한 설정 덕분에 재부팅이나 컨테이너의 재시작 시에도 안정적으로 USB 동글을 인식할 수 있게 되었다. 이는 Zigbee2mqtt의 손쉬운 관리에 큰 도움이 되었다.
백업을 넣게 된 계기
처음에는 데이터 백업의 필요성을 크게 느끼지 못했다. 실제로 서비스들이 잘 돌아가고 있었고, 문제가 발생할 가능성이 낮다고 생각했다. 하지만 예상치 못한 상황이 발생했다. 어느 날 아침, 일부 서비스에서 데이터베이스가 전혀 연결되지 않는다는 것을 발견했다. 많은 고민 끝에 데이터베이스 파일 손상이 의심되었다.
그때까지는 운영 시스템의 안정성에만 집중하고 있었고, 데이터의 복구 가능성에 대해서는 크게 신경 쓰지 않았다. 하지만 이번 일을 계기로 중요 데이터를 잃게 될 경우에 대비해야 한다는 생각이 들었다. 데이터베이스가 손상되었을 때 복구할 방법이 없는 상황이었고, 이로 인해 사용자가 데이터를 잃게 되는 것은 큰 문제였다.
그래서 데이터베이스의 주기적인 백업이 필요한 상태였다. 이 문제를 해결하기 위해서 mariadb-backup 컨테이너를 도입했다. cron을 활용해 자동으로 데이터베이스를 덤프하게 설정했다. 덕분에 데이터 유실에 대한 걱정을 덜 수 있었다. 이후에도 문제가 생겼을 때 신속하게 이전 상태로 복구할 수 있었고, 이는 서비스 운영에 큰 도움이 되었다.
하지만 백업 설정도 처음부터 순조롭지는 않았다. 잘못된 볼륨 마운트로 인해 한 번의 데이터 손실을 경험한 적이 있었다. 이를 계기로 백업 설정을 철저히 검토하고, 여러 번의 테스트를 통해 안정성을 확인했다. 이후부터는 데이터베이스 백업이 매일 자동으로 이루어져, 데이터 손실에 대한 걱정이 줄었다.
지금 상태
현재 우분투 홈서버인 monsil은 수십 개의 독립적인 서비스를 안정적으로 운영하고 있다. 각각의 서비스는 Docker 컨테이너로 구성되어 있으며, 네트워크 분리를 통해 보안을 강화했다. 모든 데이터베이스는 하나의 MariaDB 인스턴스에 연결되어 있지만, 각 서비스마다 별도의 사용자와 데이터베이스를 가지도록 분리했다. 이렇게 해서 데이터 접근과 관리가 한층 수월해졌다.
웹 서비스의 보안 강화를 위해 nginx 리버스 프록시를 사용하고 있으며, 외부 노출을 최소화하기 위해 포트를 127.0.0.1에 바인딩했다. 이는 외부의 불필요한 접근을 차단하며, 내부적으로는 서비스 간의 원활한 통신을 지원한다. 특히, 이전의 보안 이벤트 이후로는 모든 중요 로그를 주기적으로 검토하고 있다.
자동화 작업을 관리하는 n8n은 PostgreSQL 데이터베이스로의 이전과 함께 안정성이 대폭 향상되었다. 초기 설정의 실수로 데이터 손실을 경험했지만, 지금은 healthcheck 설정을 통해 안정적인 서비스 연결을 보장하고 있다. 이 과정에서 많은 시간이 걸렸지만, 현재는 만족스럽게 운영 중이다.
스마트홈 기능을 담당하는 zigbee2mqtt는 USB 동글 사용으로 인해 호스트 네트워크 모드를 활용하고 있다. 동글 연결 문제를 해결하기 위해 /dev/ttyUSB0 대신 by-id 경로를 사용하면서, 장치 인식 문제를 미연에 방지하고 있다. 이런 세심한 설정 덕분에 지금은 모든 스마트홈 장치가 원활히 작동 중이다.
함께 볼 글
이 글과 이어지는 내용은 아래에 정리해뒀다.