왜 이 글을 쓰나
서버 받으면 바로 개발부터 시작하고 싶은 마음은 이해한다. 근데 나는 그렇게 했다가 두 번 고생했다.
첫 번째는 방화벽을 켜지 않은 채로 서버를 며칠 돌렸다. 나중에 로그를 보니 수십 개의 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
'개발 팁' 카테고리의 다른 글
| 클라우드 & 공유호스팅 기본 세팅 — AWS, Railway, Vercel, 카페24 (0) | 2026.03.18 |
|---|---|
| Windows 서버 세팅 — IIS + XAMPP 실전 설정 (0) | 2026.03.18 |
| 백업 없이 MySQL/MariaDB 데이터 복구하기 (0) | 2026.03.18 |
| 정규식 실전 치트시트 — 개발자가 자주 쓰는 패턴 모음 (0) | 2026.03.18 |
| GitHub Actions 입문 — Push하면 자동으로 배포되는 CI/CD 세팅 (0) | 2026.03.18 |