

소규모 오피스 서버(Server) 구축 - Lenovo Thinkcentre M720q
리눅스(Linux): 운영체제의 “핵심(커널)” 이름. 윈도우/맥과 다른 계열.
우분투(Ubuntu): 리눅스를 바탕으로 만든 “배포판(Distribution)” 브랜드. (리눅스의 한 종류)
우분투 서버(Ubuntu Server): 우분투의 서버용 에디션(기본적으로 GUI 없이, 서버에 필요한 구성만).
그래서 정확히 쓰면:
“리눅스(계열) 중 하나인 우분투의 서버용 에디션(= Ubuntu Server 24.04 LTS)”
을 설치하자는 뜻이라서 “리눅스 우분투 서버”라고 줄여 부르는 거예요.
Desktop vs Server 차이는?
커널/명령어는 동일하고, 기본 제공 패키지와 기본 설정이 다를 뿐.
Server: GUI 없음(가벼움), SSH/네트워크/서비스 운영에 최적. 24/7 서버에 딱.
Desktop: GUI 있음(편리하지만 무거움), 개발/일상용에 편함.
당신 목적(Flask 24시간 운용, 저전력, 가성비)이면 Ubuntu Server 24.04 LTS가 정답.
정확한 이름/파일
0) M720q에 Ubuntu Server 24.04 LTS 설치를 처음부터 끝까지
준비물
1) 설치 USB 만들기 (Windows에서)
Ubuntu Server 24.04 LTS ISO 받기
파일 이름 예: ubuntu-24.04.1-live-server-amd64.iso
Rufus 실행 → USB 선택 → ISO 선택 → 옵션은 기본값(UEFI) 그대로 → Start
(BalenaEtcher를 써도 괜찮음: Etcher 실행 → ISO 선택 → USB 선택 → Flash)
팁: M720q는 UEFI 잘 지원합니다. 파티션 스킴 GPT
, Target UEFI
로 두면 OK.
2) M720q 부팅 설정
USB 꽂고 M720q 전원 ON
F12 연타 → Boot Menu에서 USB 선택 (부팅목록이 안보이면 BIOS에서 USB Boot 허용 필요)
BIOS 진입: F1 연타
필요한 설정(있으면):
Startup → CSM/Legacy: 기본 UEFI 유지
Security → Secure Boot: 기본 그대로 사용해도 보통 설치 가능 (안되면 Off)
Virtualization: 나중에 Docker에 유리하니 Enabled 추천
Auto power on after power loss: 정전 후 자동 켜짐 원하면 Enabled
저장 후 재부팅 → F12 → USB로 부팅.
3) Ubuntu Server 설치 마법사
화면 지시에 따라 순서대로:
Language: Korean(또는 English)
Keyboard: Korean(101/104 자동 인식)
Network: 유선 LAN 꽂혀 있으면 DHCP로 자동 연결됨 (Wi-Fi는 나중에 해도 됨)
Proxy / Mirror: 비워두고 넘어가도 OK
Storage:
Profile setup: 서버 사용자 만들기
SSH: Install OpenSSH server 체크 (필수)
Featured Server Snaps: 아무것도 선택하지 말고 넘어가기
설치 진행 → 완료 후 Reboot.
재부팅 직전에 설치 USB를 뽑아 주세요(계속 USB로 부팅되지 않도록).
SSH(Secure Shell) 는
“다른 컴퓨터(서버)에 안전하게 접속해서, 그 컴퓨터에서 직접 명령을 내려 쓸 수 있게 해 주는 통로”예요.
암호화돼서 도중에 엿보여도 내용이 안 풀립니다.
뭘 할 때 쓰나?
원격 서버에 로그인해서 폴더 만들고, 파일 복사하고, 프로그램 설치/실행
Flask 서버 재시작, 로그 보기, 업데이트 등 전부 터미널로 처리
파일 전송(SCP/SFTP)도 SSH 위에서 안전하게 가능
어떻게 동작해?
지금 바로 쓰는 방법 (당신 상황 기준)
1) 서버(우분투) 준비 확인
Ubuntu Server 설치할 때 OpenSSH server를 체크했다면 이미 켜져 있어요.
확인:
sudo systemctl status ssh
IP 주소 확인:
ip a # 또는 서버 화면에 표시된 IP 확인
2) 접속(내 PC → 서버)
ssh <서버사용자>@<서버IP>
# 예: ssh ubuntu@192.168.0.50
처음 접속 시 “fingerprint 신뢰하겠냐” 묻는 건 정상 → yes
.
비밀번호 대신 “키”로 접속(추천)
내 PC에서 키 만들기
ssh-keygen -t ed25519 -C "my-laptop"
# Enter, Enter, Enter로 넘어가면 ~/.ssh/id_ed25519 (개인키) / .pub(공개키) 생성
공개키를 서버에 등록
ssh-copy-id <서버사용자>@<서버IP>
# ssh-copy-id가 없으면:
# cat ~/.ssh/id_ed25519.pub 출력해서
# 서버의 ~/.ssh/authorized_keys 파일에 붙여넣기
이제부터는 비밀번호 없이:
안전하게 쓰는 습관
sudo ufw allow OpenSSH
sudo ufw enable
초간단 치트시트
# 접속
ssh user@SERVER_IP
# 파일 보내기(로컬 -> 서버)
scp local.txt user@SERVER_IP:/home/user/
# 파일 가져오기(서버 -> 로컬)
scp user@SERVER_IP:/home/user/log.txt .
# 키 생성
ssh-keygen -t ed25519 -C "my-laptop"
# 키 등록(가능하면)
ssh-copy-id user@SERVER_IP
Flask 배포 순서
“기본 보안 + Flask 서비스”
1) 기본 업데이트 & 유틸
sudo apt update && sudo apt upgrade -y
sudo apt install -y git curl htop unzip ca-certificates
sudo timedatectl set-timezone Asia/Seoul
2) 방화벽(UFW) 설정
sudo apt install -y ufw
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp # Nginx(HTTP)
# HTTPS 쓸 거면 다음도:
# sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status
3) SSH 보안(키 로그인 권장)
로컬PC에서 키가 없다면 생성:
ssh-keygen -t ed25519 -C "my-laptop"
서버에 공개키 등록(가능하면):
ssh-copy-id <서버사용자>@<서버IP>
(선택, 보안강화) 비밀번호 로그인 끄기:
sudo nano /etc/ssh/sshd_config
# 아래처럼 변경/추가
PasswordAuthentication no
PermitRootLogin no
sudo systemctl reload ssh
4) 고정 IP(선택) — 서버 안정운영에 좋음
ip a # 유선 인터페이스명 확인(ex: enp0s31f6)
sudo nano /etc/netplan/*.yaml
예시:
network:
version: 2
ethernets:
enp0s31f6:
addresses: [192.168.0.50/24]
routes:
- to: default
via: 192.168.0.1
nameservers:
addresses: [1.1.1.1,8.8.8.8]
적용:
Flask 배포 (도커 없이 깔끔 루트)
5) Nginx + Python 가상환경
sudo apt install -y nginx python3-venv python3-pip
6) 코드 가져오기
GitHub에 올린 저장소를 클론(예: firstcontainer1
):
cd ~
git clone https://github.com/<아이디>/firstcontainer1.git
cd firstcontainer1
python3 -m venv .venv
source .venv/bin/activate
# requirements.txt가 있으면:
pip install -r requirements.txt
# 없으면 최소:
pip install flask gunicorn
7) 앱 로컬 구동 테스트
gunicorn -w 2 -b 127.0.0.1:5000 app:app
8) Nginx 리버스 프록시(80 → 5000)
sudo tee /etc/nginx/sites-available/flask <<'EOF'
server {
listen 80;
server_name _;
location / {
proxy_pass http://127.0.0.1:5000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
}
}
EOF
sudo ln -s /etc/nginx/sites-available/flask /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
→ 이제 http://<서버IP>
로 접속.
9) 부팅 자동 실행(systemd)
sudo tee /etc/systemd/system/flask.service <<'EOF'
[Unit]
Description=Flask via Gunicorn
After=network.target
[Service]
User=<서버사용자>
WorkingDirectory=/home/<서버사용자>/firstcontainer1
Environment="PATH=/home/<서버사용자>/firstcontainer1/.venv/bin"
ExecStart=/home/<서버사용자>/firstcontainer1/.venv/bin/gunicorn -w 2 -b 127.0.0.1:5000 app:app
Restart=always
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now flask
sudo systemctl status flask --no-pager
<서버사용자>
를 실제 사용자명으로 바꿔 넣어줘(예: ubuntu
).
(선택) HTTPS, 자동업데이트, 모니터링
10) HTTPS (도메인 있을 때)
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d your.domain.com
11) 보안 업데이트 자동화
sudo apt install -y unattended-upgrades
sudo dpkg-reconfigure unattended-upgrades
12) 모니터링/로그 보기
sudo apt install -y glances
glances # 실시간 상태
journalctl -u flask -f # Flask 서비스 로그 실시간
BIOS 전원 옵션(유용)
정전 후 자동 켜짐:
A) Git으로 옮기기 (인증 해결됐을 때 제일 깔끔)
구름IDE 터미널에서
cd /workspace/firstcontainer1 # 네 프로젝트 폴더
git add .
git commit -m "deploy"
git branch -M main
git push -u origin main # (비번 대신 PAT 또는 SSH)
서버(우분투)에서
cd ~
git clone https://github.com/<너아이디>/firstcontainer1.git
cd firstcontainer1
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt # 없으면: pip install flask gunicorn
퍼블릭 저장소면 바로 되고, 프라이빗이면 PAT/SSH 필요(앞서 안내한 대로).
B) SSH로 직접 복사(가장 빨리 됨) — 추천
방법 B-1. scp
한 방에 복사 (구름IDE → 서버)
구름IDE 터미널에서
# 폴더 통째로 복사
scp -r /workspace/firstcontainer1 <서버사용자>@<서버IP>:/home/<서버사용자>/
# 예: scp -r /workspace/firstcontainer1 ubuntu@192.168.0.50:/home/ubuntu/
포트가 22가 아니면 -P 2222
처럼 추가.
방법 B-2. rsync
로 빠르고 반복 배포
rsync -avz --delete /workspace/firstcontainer1/ <서버사용자>@<서버IP>:/home/<서버사용자>/firstcontainer1/
# 마지막 슬래시( / ) 중요: 내용물만 동기화
방법 B-3. ZIP으로 묶어서 전송
cd /workspace/firstcontainer1
zip -r app.zip .
scp app.zip <서버사용자>@<서버IP>:~/
ssh <서버사용자>@<서버IP> 'mkdir -p ~/firstcontainer1 && unzip -o ~/app.zip -d ~/firstcontainer1 && rm ~/app.zip'
C) SFTP(그래픽 툴)로 드래그&드롭
윈도우면 WinSCP / 맥이면 Cyberduck:
옮긴 뒤 “바로 실행” 체크리스트 (서버에서)
cd ~/firstcontainer1
python3 -m venv .venv
source .venv/bin/activate
# requirements.txt 있으면:
pip install -r requirements.txt
# 없으면 최소:
pip install flask gunicorn
# 임시 실행 테스트
gunicorn -w 2 -b 127.0.0.1:5000 app:app
브라우저에서 http://서버IP:5000
열어보고 보이면 OK.
Nginx 리버스 프록시(80 → 5000) — 아직 안 했다면
sudo apt install -y nginx
sudo tee /etc/nginx/sites-available/flask <<'EOF'
server {
listen 80;
server_name _;
location / {
proxy_pass http://127.0.0.1:5000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
}
}
EOF
sudo ln -sf /etc/nginx/sites-available/flask /etc/nginx/sites-enabled/flask
sudo nginx -t && sudo systemctl reload nginx
→ 이제 http://서버IP
접속.
부팅 자동실행(systemd) — 아직 안 했다면
sudo tee /etc/systemd/system/flask.service <<'EOF'
[Unit]
Description=Flask via Gunicorn
After=network.target
[Service]
User=<서버사용자>
WorkingDirectory=/home/<서버사용자>/firstcontainer1
Environment="PATH=/home/<서버사용자>/firstcontainer1/.venv/bin"
ExecStart=/home/<서버사용자>/firstcontainer1/.venv/bin/gunicorn -w 2 -b 127.0.0.1:5000 app:app
Restart=always
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now flask
sudo systemctl status flask --no-pager
<서버사용자>
를 실제 계정명으로 바꿔줘.
.env/비밀키 주의
퍼블릭 Git엔 .env
, 비밀키 올리지 마!
이런 파일은 B 방식(scp/rsync/SFTP) 으로만 서버에 배포해서 ~/firstcontainer1/.env
에 두고,
Flask에서 python-dotenv
로 읽거나 시스템d Environment=
에 넣어.
0) 가상환경 들어가기 (공통)
cd ~/firstcontainer1
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt # 없으면: pip install flask gunicorn
1) 파일이 APPLICATION.py
이고, 안에 app = Flask(__name__)
가 있다면
(개발용, 바로 확인)
FLASK_APP=APPLICATION.py flask run --host=0.0.0.0 --port=5000
(운영용, Gunicorn)
gunicorn -w 2 -b 127.0.0.1:5000 APPLICATION:app
리눅스에서는 대소문자 구분합니다. 파일이 APPLICATION.py
면 모듈 이름도 APPLICATION입니다.
(소문자로 application:app
쓰면 import 에러 납니다)
2) “앱 팩토리” 구조(예: def create_app(): return app
)라면
(개발용)
FLASK_APP=APPLICATION.py FLASK_RUN_PORT=5000 flask run --host=0.0.0.0
(운영용)
gunicorn -w 2 -b 127.0.0.1:5000 "APPLICATION:create_app()"
3) wsgi.py
가 따로 있고 그 안에 app
이 있다면
gunicorn -w 2 -b 127.0.0.1:5000 wsgi:app
Nginx를 이미 붙여놨다면
위에서 하나를 띄운 뒤 브라우저에서:
개발용(직접): http://서버IP:5000
운영용(Nginx 프록시 통과): http://서버IP
(프록시 설정을 내가 준 그대로 썼다면 80→5000으로 전달됩니다)
부팅 자동 실행(systemd)도 ExecStart
만 맞추면 끝
(예: APPLICATION.py
+ app
전역일 때)
sudo tee /etc/systemd/system/flask.service <<'EOF'
[Unit]
Description=Flask via Gunicorn
After=network.target
[Service]
User=<서버사용자>
WorkingDirectory=/home/<서버사용자>/firstcontainer1
Environment="PATH=/home/<서버사용자>/firstcontainer1/.venv/bin"
ExecStart=/home/<서버사용자>/firstcontainer1/.venv/bin/gunicorn -w 2 -b 127.0.0.1:5000 APPLICATION:app
Restart=always
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now flask
sudo systemctl status flask --no-pager
앱 팩토리면 ExecStart= ... gunicorn ... "APPLICATION:create_app()"
로 바꿔주세요.
자주 나는 오류 체크
ModuleNotFoundError
: 대소문자/경로 틀림 → APPLICATION:app
철자 확인
AttributeError: module ... has no attribute app
: 팩토리 구조인지 확인 → create_app()
사용
Address already in use
: 이미 떠 있는 프로세스가 5000 사용 중 → pkill -f gunicorn
후 재실행
빈 페이지/502: journalctl -u flask -f
로 서비스 로그 확인
처음엔 집 와이파이/공유기 설정이 제일 헷갈립니다.
핵심은 “내 서버(우분투)까지 안정적 IP를 주고, 밖에서 안전하게 접속” 두 가지예요.
아래 3가지 루트 중 하나만 고르면 됩니다. (난 #2 또는 #3을 강력 추천)
0) 공통: 내부 IP를 고정(또는 예약)해두기
# 인터페이스명 확인 (enp0s31f6 등)
ip a
# netplan 편집
sudo nano /etc/netplan/*.yaml
# 예시
network:
version: 2
ethernets:
enp0s31f6:
addresses: [192.168.0.50/24]
routes: [{ to: default, via: 192.168.0.1 }]
nameservers: { addresses: [1.1.1.1,8.8.8.8] }
sudo netplan apply
sudo apt install -y ufw
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
# HTTPS 쓸거면: sudo ufw allow 443/tcp
sudo ufw enable
1) (가장 단순) 내부에서만 쓰기 — 포트포워딩 불필요
2) (안전·편리) Cloudflare Tunnel — 포트포워딩 없이 외부 접속
공유기·통신사 설정 복잡함을 회피하는 최적의 방법. CGNAT/이중 NAT도 통과됨.
도메인이 Cloudflare에 있다면:
# 설치
curl -fsSL https://pkg.cloudflare.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/cloudflare.gpg
echo "deb [signed-by=/usr/share/keyrings/cloudflare.gpg] https://pkg.cloudflare.com/cloudflared $(lsb_release -sc) main" | sudo tee /etc/apt/sources.list.d/cloudflared.list
sudo apt update && sudo apt install -y cloudflared
# 터널 생성/로그인
cloudflared tunnel login
cloudflared tunnel create myflask
cloudflared tunnel route dns myflask flask.my-domain.com
# 프록시(80→127.0.0.1:5000) 설정파일
sudo mkdir -p /etc/cloudflared
sudo tee /etc/cloudflared/config.yml <<'EOF'
tunnel: myflask
credentials-file: /home/ubuntu/.cloudflared/<생성된-credentials.json>
ingress:
- hostname: flask.my-domain.com
service: http://127.0.0.1:5000
- service: http_status:404
EOF
# 서비스로 등록
sudo cloudflared service install
sudo systemctl enable --now cloudflared
3) (정석) 공유기 포트포워딩 — 80/443 외부로 열기
도메인+Let’s Encrypt 직접 쓰고 싶을 때.
공유기에서 포트포워딩:
외부 80 → 내부 192.168.0.50:80
외부 443 → 내부 192.168.0.50:443
(SSH 22는 외부 개방 금지 권장. 원격관리 필요하면 WireGuard VPN 사용)
우분투에서 Nginx + 인증서:
sudo apt install -y nginx certbot python3-certbot-nginx
# Nginx 리버스프록시(80→5000) 이미 했다면 그대로 두고
sudo certbot --nginx -d flask.my-domain.com
테스트는 LTE/5G(모바일 데이터)로 접속해보기(집 와이파이=내부라 착시 생김).
⚠️ ISP가 80/443 차단/CGNAT이면 포워딩이 안 될 수 있어요. 그땐 #2 Cloudflare Tunnel 선택이 편합니다.
보너스: WireGuard VPN(원격에서 내부망처럼)
외부에서 관리/SSH만 필요하면 VPN이 제일 안전.
sudo apt install -y wireguard
# docker로 linuxserver/wireguard 써도 편함
자주 막히는 지점 체크리스트
DHCP와 고정IP 충돌: 공유기에서 IP 예약으로 해결(가장 쉬움).
더블 NAT/CGNAT: #2 Cloudflare Tunnel로 우회.
NAT Loopback 미지원: 집 와이파이에선 도메인이 안 열릴 수 있음 → LTE로 테스트.
방화벽: UFW/보안 솔루션에서 80/443, 51820(UDP) 허용 확인.
도메인 DNS 전파: A/AAAA 레코드 수정 후 수 분~수십 분 지연 가능.
추천 선택 요약
가장 쉬움/빠름: #2 Cloudflare Tunnel
전통적 직접 서비스: #3 포트포워딩 + Certbot
관리 전용(보안↑): WireGuard VPN + 내부만 공개