이번에 Oracle Cloud Platform 항상 무료 플랜도 갑자기 그냥 2대나 얻게 되는 등... 뭔가 부족한 사양이지만 무료이고 넉넉한 트래픽 용량에 블로그에는 완전 충분할 것 같아 WordPress에서 Ghost로 넘어오게 되었습니다.

사실 이 블로그를 셋업할 때도 생각보다 엄청나게 많은 시간이 소모되었습니다. 뭔가 다른 사람들과 달리 저는 Oracle Cloud라는 새로운 환경에서 예상치 못한 오류가 콘솔을 가득 메웠습니다. 그래서 차라리 Ghost-CLI 쓸바에 Node.JS 앱 돌릴게 Ghost 하나도 아니니 NVM 등을 사용하여 다른 앱도 충분히 깔끔하게 돌릴 수 있도록 구성해보도록 하겠습니다.

  • Ubuntu 패키지 업데이트
  • NVM으로 Node.JS 설치하고 환경 조성하기
  • MariaDB 데이터베이스 구성하기
  • Ghost 설치하기
  • Nginx 설치하고 보안 인증서 추가하기

모두 끝난다면 앱이 위와 같이 구성됩니다.


시작하기 전에

저의 경우에는 현재 Ubuntu LTS v16 혹은 v18의 서버 버전을 기준으로 개발을 진행하고 있습니다. 지나치게 레거시하거나 모던한 버전들은 자칫 예상치 못한 패키지 호환성 오류로 여러분을 고생하게 만들 수도 있거든요. 아래 환경에서 설치할 예정입니다.

  • Ubuntu 18.04.3 LTS (standard) on Oracle Cloud (always free plan)

서버 패키지 업데이트하기

언제나 시작하기 전에 패키지 업데이트는 필수입니다. 서버는 개인용 컴퓨터처럼 짧은 시간 내에 높은 가용성을 보여주어야 하는 컴퓨터가 아니라 오랜 시간 안정적으로 실행될 수 있는 상태여야 합니다. 이후에 불필요한 패키지 업데이트는 자칫 예상치 못한 오류를 일으킬 수도 있으니 셋업 시에 업데이트를 포함시켜주세요. 혹시라도 필요하신 분들은 이후에도 불필요한 짜증을 줄이기 위해 미러 사이트도 업데이트해주시면 좋습니다.

# Promote permission level to `admin`
sudo su

# Pull updates from apt repository server
apt update --fix-missing

# Upgrade packages
apt upgrade -y

애플리케이션용 사용자 만들기

Root 사용자로 앱을 실행해도 안전한 경우가 있긴 하지만 보통은 기본적인 권한 수준을 떨어뜨려 앱을 관리하는 것이 안전합니다. 이 경우에는 로컬 사용자 계정에 소프트웨어를 설치해야 하니 관리자 권한이 필요한 명령에는 sudo를 앞에 붙이도록하고 어떤 소프트웨어가 어떤 사용자 아래에서 실행되고 있는지 숙지하도록 해야 합니다.

# Promote permission level to `admin`
sudo su

# Create new user with home directory
adduser <username>

Node.JS 설치하기

Node.JS의 관습으로 변화를 지향하더라도 이전 메서드를 그대로 계속 지원하는 경향이 있어 보통은 Node.JS의 버전을 중간에 업그레이드해도 오류가 발생하지 않습니다. 하지만 Ghost와 같이 빌드해야 하는 네이티브 패키지를 사용하는 경우에는 특정 패키지가 빌드에 실패하거나 스펙이 맞지 않는 경우도 있어 NVM으로 Node.JS의 버전을 관리해줍니다.

NVM으로 Node.JS 버전 관리하기

NVM은 약자 그대로 Node Version Manager로 Linux 혹은 UNIX 환경에서 사용가능한 Node.JS 버전 관리자입니다. NVM 하나로 여러 Node.JS 버전을 손쉽게 변경할 수 있게 되어 특히 개발 환경에서 편리합니다. 하지만 꼭 그렇지 않더라도 Node.JS 저장소를 업데이트하여 버전을 변경하는 것보다는 훨씬 깔끔하기 때문에 서버에서도 주로 사용하고 있습니다.

nvm-sh/nvm
Node Version Manager - POSIX-compliant bash script to manage multiple active node.js versions - nvm-sh/nvm

위 공식 GitHub 저장소는 NVM의 공식 저장소입니다. 아래에도 명령을 첨부할 예정이지만 혹시라도 변경될 수 있으니 참고해주세요.

# Install NVM
wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.3/install.sh | bash
  • 앞에서 말했듯이 NVM은 로컬 사용자에게도 설치될 수 있으니 소프트웨어가 어떤 사용자 밑에서 구성되고 실행되는지 주의해주세요.

설치가 완료되고 쉘에 다시 연결하면 NVM이 정상적으로 작동하는 것을 확인하실 수 있습니다. 그 후에는 아래와 같이 원하시는 Node.JS 버전을 설치해주시면 됩니다. 설치가 되면 아무것도 설치된 버전이 없으실테니 자동으로 설치하신 버전이 활성화됩니다. 현재 Ghost같은 경우에는 LTS 릴리즈 중 v10 혹은 v12를 중심으로 지원되고 있습니다. 보통의 경우에는 v12를 설치하시면 되지만 저와 같은 경우에는 다른 앱에서 네이티브 패키지가 호환이 되지 않기 때문에 v10을 사용합니다.

# Fetch the list of available Node.JS versions
nvm ls-remote | grep v10

# Install the latest version of v10 (2020-05-15)
nvm install v10.20.1

Yarnpkg 패키지 매니저 설치하기

은근히 요즘 많은 패키지들이 NPM 대신 Yarn을 선택하는 경우가 있습니다. 물론 둘 다 정상적으로 동작하겠지만 더 빠른 속도를 위해 저는 Yarn을 사용합니다. 이 부분은 선택적이므로 NPM을 사용하셔도 괜찮습니다. 아래 웹 사이트에서 알맞은 운영체제를 선택하여 설치해주시면 됩니다.

https://classic.yarnpkg.com/en/docs/install#debian-stable

저는 Ubuntu 18.04.3 LTS를 사용하고 NVM을 통해 Node.JS를 설치했으니 Yarn을 설치할 경우 자동으로 권장되는 패키지에 포함되는 Ubuntu 저장소의 Node.JS를 배제해야 합니다. 그러므로 실제 명령은 다음과 같아집니다.

# Add the repository of yarnpkg
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list

# Install yarn without Node.JS from official Ubuntu repository
apt update && apt install --no-install-recommends yarn
  • 만약 Yarn을 이미 설치한 것 같지만 뭔가 평소와 같지 않은 문자열을 보게 되신다면 Ubuntu 공식 저장소에서 다른 패키지를 설치하신 것일 수 있으니 꼭 Yarn 저장소를 설치하기 전에 추가해주세요.

PM2 프로세스 매니저 설치하기

마지막으로 프로세스 매니저를 사용하여 앱이 항시 작동하도록 최소한의 보장을 해주어야 합니다. 그냥 node 명령으로 앱을 실행하는 도중에 중간에 한 번이라도 Exception이 발생하면 그대로 앱의 라이프사이클은 멈춰버리기 때문입니다. Forever, Nodemon 등의 프로세스 매니저가 존재하지만 가장 다재다능한 PM2를 통해 프로세스를 관리할 예정입니다. 또한 PM2는 Node.JS에만 한정되는 프로세스 매니저가 아니기도 합니다. PM2는 전역으로 설치해주면 됩니다.

  • 저와 같은 경우에는 Oracle Cloud에서 Yarn이 이상하게 전역으로 패키지를 설치하지 못하는 케이스가 생겨 NPM을 대신 사용했습니다.
# Install PM2 globally via Yarn
yarn global add pm2

# Install PM2 globally via NPM
npm i -g pm2

설치가 끝나면 한 번 실행시켜 초기화한 다음 시스템 시작 시에 자동으로 실행되게 만들어줍니다.

# Start up PM2 master process once
pm2 ls

# Launch PM2 on system start up
pm2 startup

C++ 컴파일러 설치하기

Ghost 앱에는 Knex 쿼리 빌더 등등 네이티브 패키지가 포함되어 있어 C++ 코드를 빌드해주어야 합니다. 리눅스에서는 귀찮은 작업없이 이를 단순히 build-essential 패키지 하나로 해결할 수 있습니다.

# Install build-essential from official Ubuntu repository
apt install build-essential

MariaDB 데이터베이스 설치하기

이제 데이터베이스를 설치합니다. 여기에서는 기본적인 SQL 명령만 다루고 있으니 조금 더 복잡한 상황에 있으신 경우에는 사용자 간 권한이나 관계에 주의하셔야 합니다.

  • 여기에서 제가 서술하는 SQL 구문에서 따옴표는 구분을 위한 목적이 아닌 SQL에 사용되는 일부이기 때문에 생략하면 안 됩니다. 필요한 요소들은 <key>와 같이 서술했습니다.

먼저 MariaDB 서버 패키지를 설치합니다. Ubuntu의 공식 저장소에서 바로 설치하실 수 있습니다. 그리고 간단한 설정을 한 번 거치시면 설치가 완료됩니다.

# Install MariaDB server from official repository
apt install mariadb-server

# Configure the package
mysql_secure_installation

데이터베이스 생성 후 사용자 추가하기

이제 Ghost에 필요한 데이터베이스를 생성할 차례입니다. mysql 혹은 mariadb를 콘솔에 입력하면 데이터베이스 쉘에 진입할 수 있습니다. 편의를 위해 root 계정으로 작업합니다.

create database <database_name>;
create user '<username>'@'localhost' identified by '<password>';
grant all privileges on <database_name>.* to '<username>'@'localhost' identified by '<password>';

첫 번째 구문부터:

  1. 데이터베이스 생성
  2. 사용자 생성
  3. 생성한 데이터베이스의 모든 권한을 생성한 사용자에게 부여

여기까지 오셨다면 데이터베이스 테이블 등은 처음 시작 시 자동으로 생성되니 이제 내부적인 환경은 모두 완료된 것입니다.

Ghost 설치하기

먼저 Ghost를 설치할 위치를 결정해야 합니다. 저는 홈 디렉터리를 고민하다가 공식 가이드북과 같이 /var/www 웹 디렉터리에 설치하는 것으로 하였습니다.

/var/www 아래에 새로운 디렉터리를 만들고 권한을 웹 애플리케이션을 위한 775로 설정합니다.

# Create new directory: /var/www/blog
mkdir /var/www/blog

# Update permission* of: /var/www/blog
chmod 775 /var/www/blog

만약 다른 사용자를 통하여 Ghost를 실행하실 예정이라면 디렉터리의 소유자도 변경하셔야 합니다. 아래와 같이 chown 명령에 R 플래그를 붙이면 recursive 옵션이 적용되어 디렉터리와 하위 디렉터리에 모두 적용됩니다.

# Update owner of the directory
chown <user>:<group> -R /var/www/blog

Ghost 다운로드 후 의존성 설치

먼저 Ghost의 마지막 버전을 다운로드하고 의존성을 설치합니다. Git을 통해 Clone해도 됩니다. 여기에서는 Zip 아카이브를 다운로드하는 것으로 하겠습니다. 만약 Git을 통해 설치하실 예정이라면 아래 공식 저장소를 Clone한 뒤 올바른 태그로 Reset 해주시면 되겠습니다.

TryGhost/Ghost
👻 The #1 headless Node.js CMS for professional publishing - TryGhost/Ghost
# Install unzip package from official Ubuntu repository
apt install unzip

# Goto source directory
cd /var/www/blog

# Download latest Ghost ZIP archive
curl -L https://ghost.org/zip/ghost-latest.zip -o ghost-latest.zip

# Unzip the archive and remove it from directory
unzip ghost-latest.zip
rm ghost-latest.zip

# Install dependencies via Yarn
yarn

Ghost 애플리케이션 구성하기

이제 PM2 ecosystem config와 Ghost config를 구성합니다. 먼저 ecosystem.config.js 파일은 PM2에서 어떤 파일을 관리해야 할지 알려주는 역할을 합니다. Ghost의 기본적인 엔트리 스크립트는 package.json에서도 확인하실 수 있듯 index.js입니다. 환경변수도 그에 맞춰서 아래 내용을 ecosystem.config.js에 포함시켜줍니다.

module.exports = {
  apps : [
      {
        name: "blog",
        script: "./index.js",
        watch: false,
        env: {
          "NODE_ENV": "production",
        }
      }
  ]
}

다음으로는 Ghost 앱 구성 설정을 진행합니다. config.development.jsonconfig.production.json으로 복사하고 환경에 따라 설정해주시면 됩니다. 저는 기본 포트인 2368번 포트는 놀고 있기에 포트는 건드리지 않았습니다. 참고로 현재 가이드에서는 추후에 Nginx로 리버스 프록시를 구성할 계획이기 때문에 내부적으로는 url 값을 http 스키마로 작성하셔야 합니다. 그렇지 않으면 내부에서는 HTTP 프로토콜로 동작하기 때문에 https로 무한정 리다이렉션이 발생하게 됩니다.

{
    "url": "http://domain.tld",
    "database": {
        "client": "mysql",
        "connection": {
            "host"     : "127.0.0.1",
            "user"     : "<database_user>",
            "password" : "<database_password>",
            "database" : "<database_name>"
        }
    },
    "auth": {
        "type": "password"
    },
    "paths": {
        "contentPath": "content/"
    },
    "logging": {
        "level": "info",
        "rotation": {
            "enabled": true
        },
        "transports": ["file", "stdout"]
    }
}

Ghost 애플리케이션 실행하기

이제 Ghost 애플리케이션 셋업이 모두 완료되었으니 실행을 한 번 해줍니다. 첫 실행 시에는 테이블 등 실행에 필요한 에셋이 모두 생성되니 시간이 조금 걸릴 수 있습니다. PM2로 ecosystem.config.js를 로드합니다.

# Start nodes via ecosystem.config
pm2 start ecosystem.config.js

쨘! 이제 localhost의 2386 포트에서는 정상적으로 앱이 동작하고 있을 것입니다. 마지막으로 Nginx 리버스 프록시를 셋업해주도록 합니다.

Nginx 리버스 프록시 구성하기

정말 이제 마지막 단계입니다. 향후 부가적인 웹 애플리케이션 보안을 위한 일종으로 Nginx 리버스 프록시를 주로 사용합니다. 전문적인 작업은 전문가인 Nginx에게 맡기는 것과 같습니다. Nginx는 입맛에 따라서 구성이 많이 달라질 수 있는데 저는 snippets를 사용하여 사이트 config 파일을 최대한 짧게 유지하는 것을 좋아합니다. 만약 제 스니펫을 사용하시고 싶으시다면 아래 저장소를 사용해주시면 되겠습니다.

Seia-Soto/nginx-conf
Snippets from `seia.io`. Contribute to Seia-Soto/nginx-conf development by creating an account on GitHub.

먼저 Nginx를 서버에 설치합니다. 이미 80번 포트가 사용 중이라면 설치 후 시작 단계에서 오류를 내뿜을 것이니 포트가 사용 중인지 미리 확인해주세요. 그리고 모든 웹의 최외곽 작업은 Nginx가 담당할 예정이니깐요.

# Install Nginx from official Ubuntu repository
apt install nginx

default 가상호스트 파일 수정하기

가상호스트는 한 서버에서 여러 웹 사이트를 구성할 수 있도록 해줍니다. 사용자가 같은 IP주소로 접속하더라도 실제 접속할 도메인은 HTTP의 SNI 필드에 서술되어 있기 때문에 웹 서버는 사용자를 분리할 수 있게 됩니다. 기본적으로 Nginx에서 default 가상호스트는 서버로 오는 모든 요청을 받게 되어 있는데 보안 상 해커가 특정 사이트에 HTTPS가 아닌 HTTP 프로토콜로 접근 가능하게 만들어주게 됩니다. 그러므로 이를 기본적으로 삭제하거나 모든 요청에 응답하지 않는 것으로 설정합니다. Nginx에서는 444 등 특수한 리턴 코드를 추가로 설정할 수 있습니다. 이 때 444는 요청에 응답하지 않고 연결을 버린다는 것을 의미합니다.

# Goto virtual host directory
cd /etc/nginx/sites-available

# Copy original default file
cp default default.old

가상 호스트 설정 파일은 sites-enabled 폴더에 위치해 있으며 sites-available 폴더에 원본을 두고 쉽게 말해 바로가기와 같은 역할을 하는 symlink를 생성하여 관리하게 됩니다. 지금은 먼저 default 파일을 아래와 같이 수정하여 모든 요청에 대해 기본적으로 444를 리턴하도록 만들어줍니다.

server {
        listen 80 default_server;
        listen [::]:80 default_server;

        server_name _;

        return 444;
}

Ghost 웹 사이트 Nginx 구성 파일 만들기

여기에서부터는 여러 사이트에 걸쳐 사용될 수 있는 부분을 snippet을 사용해 단순화하였으니 전체 구성 설정을 보려면 위에 올린 Seia-Soto/nginx-conf 저장소를 참고해주세요. 먼저 /etc/nginx/sites-available/<domain.tld> 파일을 만들어줍니다.

server {
        listen 80;
        listen 443 ssl http2;

        server_name b2.seia.io;

        ssl_certificate /etc/letsencrypt/live/b2.seia.io/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/b2.seia.io/privkey.pem;

        location / {
                include snippets/use-nodeproxy.conf;

                proxy_pass http://localhost:2368;
                proxy_redirect off;
        }

        client_max_body_size 16M;

        include snippets/use-certificate.conf;
        include snippets/use-headers.conf;
}
  • server_name 값에는 사용자의 도메인이 들어가도록 합니다. *와 같은 문자도 사용할 수 있습니다.
  • client_max_body_size 값을 통해 파일의 최대 업로드 크기를 결정합니다. 이는 최소한 8M 이상으로 설정하여 웹 사이트의 헤더나 로고 업로드에 지장을 최소화하는 것이 좋습니다.

그리고 아래 명령어로 sites-enabled 디렉터리에 설정 파일의 바로가기 역할을 한다고 볼 수 있는 symlink를 만들어줍니다.

# Create symlink of the configuration file
ln -s /etc/nginx/sites-available/<domain.tld> /etc/nginx/sites-enabled/<domain.tld>

가상호스트 파일 저장 이후 바로 적용되는 것이 아니기 때문에 인증서는 이후에 발급해도 괜찮습니다. 현재는 Let's Encrypt 패키지에서 발급된 인증서가 저장되는 기본 경로를 적어두었습니다.

SSL 인증서 발급하기

Let's Encrypt를 사용해 SSL 연결을 보장함으로써 최소한의 보안을 지키도록 합니다. 여기에서는 DNS 기반 수동 인증을 사용합니다. 3개월 전에 다시 발급을 해주신 다음 service nginx reload 명령을 통해 Nginx 설정을 다시 로드하시면 됩니다.

# Install letsencrypt (certbot) package from official Ubuntu repository
apt install letsencrypt

# Request a new certificate via DNS authentication
letsencrypt --manual --preferred-challenges dns certonly

서비스 시작하기

이제 마지막으로 nginx -t 명령을 통해 현재 설정에서 오류를 검출하고 service nginx reload 명령을 사용하여 웹 서버를 재시작시키면 여러분의 도메인에 멋진 하나의 Ghost 사이트가 탄생하여 있을 것입니다. =ㅅ=;