본문 바로가기
개발 팁

Linux 서버 초기 세팅 — 처음 서버 받으면 이것부터

by 루까(Luka) 2026. 3. 18.
반응형

왜 이 글을 쓰나

서버 받으면 바로 개발부터 시작하고 싶은 마음은 이해한다. 근데 나는 그렇게 했다가 두 번 고생했다.

첫 번째는 방화벽을 켜지 않은 채로 서버를 며칠 돌렸다. 나중에 로그를 보니 수십 개의 IP에서 포트 스캔이 찍혀 있었다. 다행히 비밀번호가 복잡해서 뚫리지는 않았지만 찝찝했다.

두 번째는 루트 계정으로 MySQL 작업을 하다가 실수로 DROP DATABASE를 날렸다. binlog가 꺼져 있어서 복구 불가였다. 그날 반나절을 날렸다.

이 글은 서버를 처음 받았을 때 5~10분 안에 해두면 나중에 편한 것들을 정리한다. 하나씩 따라하면 된다.


SSH 설정

키 기반 인증

비밀번호로 SSH 접속하는 건 편하지만 무차별 대입 공격에 취약하다. 키 기반 인증으로 바꾸는 게 맞다.

로컬 머신에서 키 페어 생성:

ssh-keygen -t ed25519 -C "your@email.com"

공개키를 서버에 등록:

ssh-copy-id -i ~/.ssh/id_ed25519.pub user@server-ip

또는 수동으로:

cat ~/.ssh/id_ed25519.pub >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys

sshd_config 설정

키 기반 인증이 정상 동작하는 걸 확인한 뒤에 비밀번호 로그인을 비활성화한다. 순서를 지켜야 한다. 먼저 비활성화하면 서버에서 잠긴다.

sudo nano /etc/ssh/sshd_config

수정할 항목:

Port 2222                     # 기본 22에서 변경 (선택, 아래 주의사항 참고)
PermitRootLogin no            # 루트 직접 로그인 차단
PasswordAuthentication no     # 비밀번호 인증 비활성화
PubkeyAuthentication yes      # 키 기반 인증 활성화
AuthorizedKeysFile .ssh/authorized_keys

설정 반영:

sudo systemctl restart sshd

포트를 바꿨다면 ssh -p 2222 user@server-ip 형태로 접속한다.

포트 변경 시 주의사항: sshd_config 수정 → ufw에서 새 포트 허용 → sshd 재시작 → 새 포트로 접속 테스트까지 확인한 뒤 기존 포트를 닫는다. 순서를 어기면 서버에서 잠긴다. Ubuntu 24.04에서는 /etc/ssh/sshd_config.d/ 디렉터리 아래 드롭인 파일로 설정을 분리하는 방식도 권장된다.


방화벽 (ufw)

Ubuntu 24.04 기준 ufw가 기본 설치되어 있다. CentOS는 firewalld를 쓰지만 원리는 같다.

# 기본 정책: 모두 차단
sudo ufw default deny incoming
sudo ufw default allow outgoing

# 필요한 포트만 열기
sudo ufw allow 22        # SSH (포트 바꿨으면 해당 포트로)
sudo ufw allow 80        # HTTP
sudo ufw allow 443       # HTTPS

# 활성화
sudo ufw enable

# 상태 확인
sudo ufw status verbose

포트를 바꿨다면 22 대신 새 포트를 열어야 한다. ufw enable 전에 반드시 SSH 포트를 열어둘 것. 안 그러면 접속이 끊긴다.

fail2ban 설치 (권장)

브루트포스 시도를 자동으로 차단한다. SSH 포트를 열어두는 이상 기본으로 설치하는 게 맞다:

sudo apt install fail2ban

# 로컬 설정 파일 생성 (원본 jail.conf는 건드리지 않는다)
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
sudo nano /etc/fail2ban/jail.local

[sshd] 섹션에서 확인할 항목:

[sshd]
enabled  = true
port     = ssh          # 포트를 바꿨다면 실제 포트 번호로
maxretry = 5            # 5회 실패 시 차단
bantime  = 3600         # 1시간 차단 (초 단위, -1이면 영구)
findtime = 600          # 10분 내 maxretry 회 실패 시 차단
sudo systemctl enable fail2ban
sudo systemctl start fail2ban

# 차단 현황 확인
sudo fail2ban-client status sshd

MySQL 원격 접속이 필요하다면:

sudo ufw allow from 192.168.1.0/24 to any port 3306

특정 IP 대역에만 허용하는 게 맞다. 전체 오픈은 하지 않는다.


FTP / SFTP 설정

FTP vs SFTP

FTP는 평문 전송이다. 네트워크에서 패킷을 캡처하면 아이디, 비밀번호, 파일 내용이 그대로 보인다. 공개 네트워크에서는 쓰지 않는 게 맞다.

항목 FTP SFTP
전송 방식 평문 SSH 암호화
포트 21 22 (SSH와 동일)
방화벽 설정 복잡 (passive mode) SSH 포트 하나만
인증 비밀번호 비밀번호 / 키 인증
권장 여부 내부망 한정 항상

내부망이 아니라면 SFTP를 쓴다. OpenSSH 설치만 되어 있으면 SFTP는 별도 설정 없이 바로 된다.

vsftpd (내부망 한정)

외부망에서 FTP가 꼭 필요한 경우에만 설치한다:

sudo apt install vsftpd
sudo nano /etc/vsftpd.conf

주요 설정:

anonymous_enable=NO
local_enable=YES
write_enable=YES
chroot_local_user=YES
ssl_enable=YES          # FTPS 활성화
sudo systemctl restart vsftpd
sudo ufw allow 21

FileZilla SFTP 연결

FileZilla에서 SFTP로 연결할 때: 파일 → 사이트 관리자 → 새 사이트
프로토콜: SFTP - SSH File Transfer Protocol
호스트: 서버 IP
로그온 유형: 키 파일
키 파일: ~/.ssh/id_ed25519 (로컬 경로)


웹서버 설정

Nginx vs Apache

항목 Nginx Apache
처리 방식 비동기 이벤트 기반 요청마다 스레드/프로세스
정적 파일 성능 빠름 상대적으로 느림
.htaccess 미지원 지원
설정 구조 단순 모듈 기반, 유연
리버스 프록시 우수 가능하지만 복잡
적합 용도 트래픽 많은 서비스, Node.js 프록시 PHP 앱, 레거시 시스템

신규 프로젝트라면 Nginx를 권장한다. 설정이 단순하고 리버스 프록시 구성이 편하다.

Nginx 가상호스트 설정

sudo nano /etc/nginx/sites-available/mysite.conf
server {
    listen 80;
    server_name example.com www.example.com;
    root /var/www/mysite;
    index index.html index.php;

    location / {
        try_files $uri $uri/ =404;
    }

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php8.2-fpm.sock;
    }

    access_log /var/log/nginx/mysite.access.log;
    error_log /var/log/nginx/mysite.error.log;
}

심볼릭 링크로 활성화:

sudo ln -s /etc/nginx/sites-available/mysite.conf /etc/nginx/sites-enabled/
sudo nginx -t        # 설정 문법 확인
sudo systemctl reload nginx

sites-available에 설정 파일을 두고, sites-enabled에 심볼릭 링크를 걸어서 사용하는 구조다. 비활성화할 때는 링크만 지우면 된다.

Apache .htaccess

Apache를 쓴다면 .htaccess로 디렉토리 단위 설정을 적용할 수 있다. 이 기능을 쓰려면 AllowOverride All이 활성화되어 있어야 한다:

# /etc/apache2/sites-available/mysite.conf
<Directory /var/www/mysite>
    AllowOverride All
</Directory>
# .htaccess 기본 예시 (Laravel, WordPress 등)
<IfModule mod_rewrite.c>
    RewriteEngine On
    RewriteBase /
    RewriteRule ^index\.php$ - [L]
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteRule . /index.php [L]
</IfModule>

MySQL / MariaDB 초기 설정

버전 선택

Ubuntu 24.04 기준 패키지 저장소에서 제공하는 버전:

DB 버전 특이사항
MySQL 8.4 LTS Innovation 트랙과 별개로 장기 지원, 2026년 기준 주력 LTS
MariaDB 11.4 LTS 2029년까지 지원, MySQL 8.0 호환

MySQL 8.4에서 mysql_secure_installation의 일부 동작이 바뀌었다. 기존에 프롬프트로 묻던 VALIDATE PASSWORD 컴포넌트 활성화가 interactive 실행 시 생략될 수 있다. 직접 확인하는 게 안전하다.

mysql_secure_installation

설치 직후 반드시 실행한다:

sudo mysql_secure_installation

물어보는 것들 (MySQL 8.4 기준):

  • VALIDATE PASSWORD 컴포넌트 활성화 여부 (선택, 권장)
  • 루트 비밀번호 설정 (auth_socket 방식 사용 시 생략될 수 있음)
  • 익명 사용자 제거 → Y
  • 원격 루트 로그인 차단 → Y
  • test 데이터베이스 제거 → Y
  • 권한 테이블 재로드 → Y

MySQL 8.4에서는 SET PASSWORD 구문이 deprecated됐다. 비밀번호 변경은 ALTER USER를 쓴다:

ALTER USER 'root'@'localhost' IDENTIFIED BY 'new_password';
FLUSH PRIVILEGES;

binlog 활성화

나중에 데이터 복구가 필요할 때 binlog가 없으면 방법이 없다. 처음부터 켜둔다.

sudo nano /etc/mysql/mysql.conf.d/mysqld.cnf
[mysqld]
log_bin = /var/log/mysql/mysql-bin.log
binlog_expire_logs_seconds = 604800   # 7일 보관
max_binlog_size = 100M
server-id = 1
sudo systemctl restart mysql

사용자/권한 분리

루트 계정을 직접 쓰지 않는다. 프로젝트마다 전용 계정을 만든다:

-- 새 사용자 생성
CREATE USER 'appuser'@'localhost' IDENTIFIED BY 'strong_password';

-- 특정 DB에만 권한 부여
GRANT SELECT, INSERT, UPDATE, DELETE ON myapp.* TO 'appuser'@'localhost';

-- 원격 접속이 필요한 경우 (IP 지정 권장)
CREATE USER 'appuser'@'192.168.1.10' IDENTIFIED BY 'strong_password';
GRANT SELECT, INSERT, UPDATE ON myapp.* TO 'appuser'@'192.168.1.10';

FLUSH PRIVILEGES;

원격 접속을 허용하려면 bind-address도 조정해야 한다:

# mysqld.cnf
bind-address = 0.0.0.0    # 전체 허용 (방화벽에서 IP 제한 필수)
# bind-address = 127.0.0.1  # 로컬만 (기본값, 원격 차단)

언어별 설치 후 설정

PHP

sudo nano /etc/php/8.2/fpm/php.ini

확인할 항목:

upload_max_filesize = 64M     # 기본 2M → 파일 업로드 필요시 변경
post_max_size = 64M
max_execution_time = 60       # 기본 30초
memory_limit = 256M
display_errors = Off          # 운영 서버에서는 반드시 Off
log_errors = On
error_log = /var/log/php_errors.log
sudo systemctl restart php8.2-fpm

Node.js 설치

2026년 기준 활성 LTS는 Node.js 22.x다. nvm으로 설치하는 걸 권장한다:

# nvm 설치
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.0/install.sh | bash
source ~/.bashrc   # 또는 ~/.zshrc

# LTS 최신 버전 설치 (현재 22.x)
nvm install --lts
nvm use --lts

# 버전 확인
node -v   # v22.x.x
npm -v

nvm 대신 fnm(Fast Node Manager)도 좋은 대안이다. Rust로 작성돼 설치/전환이 더 빠르다:

# fnm 설치
curl -fsSL https://fnm.vercel.app/install | bash
source ~/.bashrc

# LTS 설치 및 사용
fnm install --lts
fnm use lts-latest

Node.js — PM2로 프로세스 관리

Node.js 앱을 그냥 node app.js로 실행하면 터미널 닫으면 죽는다. PM2를 쓴다:

npm install -g pm2   # PM2 5.x 설치

# 앱 실행
pm2 start app.js --name myapp

# 서버 재시작 시 자동 시작 (startup 명령 출력된 스크립트 복붙해서 실행)
pm2 startup
pm2 save

# 상태 확인
pm2 status
pm2 logs myapp
pm2 monit    # 실시간 모니터링 대시보드

pm2 startup 실행 시 출력되는 sudo env PATH=... 형태의 명령을 그대로 복사해서 실행해야 자동 시작이 등록된다. 출력을 무시하고 넘어가면 재부팅 시 앱이 안 뜬다.

Python — venv 설정

시스템 Python을 오염시키지 않으려면 가상 환경을 쓴다:

python3 -m venv /var/www/myapp/venv
source /var/www/myapp/venv/bin/activate
pip install -r requirements.txt

시스템 서비스로 등록하려면 /etc/systemd/system/myapp.service를 만들어서 관리한다.


자동화

crontab DB 백업

crontab -e
# 매일 새벽 2시에 DB 백업
0 2 * * * /usr/bin/mysqldump -u appuser -p'strong_password' myapp > /backup/myapp_$(date +\%Y\%m\%d).sql

# 30일 지난 백업 파일 삭제
0 3 * * * find /backup -name "*.sql" -mtime +30 -delete

백업 파일에 날짜가 붙으니 30일치가 쌓인다. 용량 확인을 주기적으로 해야 한다.

unattended-upgrades (자동 보안 업데이트)

보안 패치는 수동으로 챙기기 어렵다. Ubuntu 24.04에서는 unattended-upgrades가 기본 설치되어 있지만 활성화 여부를 확인해야 한다:

sudo apt install unattended-upgrades   # 미설치 시
sudo dpkg-reconfigure --priority=low unattended-upgrades

설정 파일 확인:

sudo nano /etc/apt/apt.conf.d/50unattended-upgrades

보안 업데이트만 자동 적용하려면 아래 줄이 활성화되어 있어야 한다:

Unattended-Upgrade::Allowed-Origins {
    "${distro_id}:${distro_codename}-security";
    // "${distro_id}:${distro_codename}-updates";  // 선택: 일반 업데이트도 포함
};

자동 재부팅이 필요한 경우 (Automatic-Reboot "true")는 운영 서버에서 신중하게 결정한다. 보안 패치만 적용하고 재부팅은 수동으로 하는 게 일반적이다:

# 적용 현황 확인
sudo unattended-upgrades --dry-run
cat /var/log/unattended-upgrades/unattended-upgrades.log

logrotate

로그 파일이 무한정 커지지 않게 관리한다:

sudo nano /etc/logrotate.d/myapp
/var/log/nginx/mysite.*.log {
    daily
    missingok
    rotate 14
    compress
    delaycompress
    notifempty
    sharedscripts
    postrotate
        nginx -s reopen
    endscript
}
sudo logrotate -d /etc/logrotate.d/myapp   # 드라이런 테스트

삽질 기록

루트로 개발했다가 권한 꼬임

처음 서버 받고 귀찮아서 루트로 파일 만들고 웹서버 설정하고 다 했다. 나중에 www-data 계정으로 실행되는 PHP에서 파일 쓰기 권한 에러가 계속 났다. 파일 소유자가 root라서 PHP가 못 쓰는 거였다.

결국 chown -R www-data:www-data /var/www/mysite로 소유자를 바꿨는데, 이걸 하다 보니 실수로 시스템 파일 경로까지 범위가 걸려서 더 꼬였다. 배포 계정을 처음부터 분리하고, 루트는 시스템 설정에만 쓰는 게 맞다.

방화벽 안 켜서 포트 스캔 당함

빠르게 개발하려고 방화벽 설정을 "나중에"로 미뤘다. 3일 뒤 접속 로그를 보니 모르는 IP 수십 개가 22, 3306, 8080 등 다양한 포트를 훑어간 흔적이 있었다. 비밀번호가 강력해서 실제 침입은 없었지만, ufw 켜는 게 2분도 안 걸리는 일인데 미룬 게 바보 같았다.

binlog 꺼져있어서 DB 복구 못 한 케이스

테스트 중에 DROP TABLE을 잘못 날렸다. 5분 전까지 데이터가 있었는데 방법이 없었다. mysqldump 백업도 전날 것이라 하루치 데이터가 날아갔다. binlog가 켜져 있었으면 mysqlbinlog로 해당 시점 직전까지 복구할 수 있었다.

지금은 서버 받으면 binlog 활성화가 MySQL 설정에서 가장 먼저 하는 일이 됐다.

SSH 포트 바꾸고 ufw에서 새 포트 안 열음

포트를 22에서 2222로 바꾸고 ufw enable을 했는데 SSH 접속이 끊겼다. ufw 기본 정책이 deny incoming이라 2222 포트가 막혀 있었던 것. 서버 콘솔(VNC)로 간신히 접속해서 살렸다. 포트 바꾸는 순서는 반드시 sshd_config 수정 → 새 포트 ufw allow → sshd restart → 새 포트로 접속 테스트 → 기존 포트 ufw delete다.


마무리

서버 초기 세팅은 개발 자체보다 훨씬 단순한 일이다. 그런데 한 번 빠뜨리면 나중에 찾기가 어렵고, 터지면 복구하는 게 더 힘들다.

요약하면 이렇다:

  • SSH는 키 인증으로, 루트 로그인은 차단
  • 방화벽은 처음부터, 필요한 포트만
  • fail2ban으로 브루트포스 차단
  • FTP 대신 SFTP
  • MySQL은 전용 계정, binlog 켜기
  • unattended-upgrades로 보안 패치 자동 적용
  • 백업 자동화는 설치 당일

이 중 하나라도 빠지면 언제 터질지 모른다. 처음 30분 투자하면 그 뒤로 신경 쓸 일이 없다.


OS: Ubuntu 24.04 LTS / CentOS Stream 9
웹서버: Nginx / Apache
DB: MySQL 8.4 LTS / MariaDB 11.4 LTS
Node.js: v22.x LTS (nvm / fnm)
프로세스 관리: PM2 5.x

반응형