saip.learning notes

Lightsail 서버와 GitHub·ghcr로 첫 배포

배포 · 마지막 수정 2026-10-24

로컬에서 리허설한 Docker 구성을 실제 클라우드 서버에 올린 기록이다. 코드는 GitHub에, 이미지는 GitHub Container Registry(ghcr)에, 서버는 AWS Lightsail에 둔다.

flowchart LR
  pc["`**내 PC**
  코드, docker build`"] -- "git push" --> gh[("GitHub 저장소")]
  pc -- "docker push" --> ghcr[("ghcr.io 이미지 창고")]
  ghcr -- "docker pull" --> server["`**Lightsail 서버**
  docker compose up`"]
  pc -. "ssh (키 파일)" .-> server

Lightsail: 월 정액 VPS

**VPS(가상 사설 서버)**는 데이터센터의 큰 컴퓨터를 잘라 빌려 쓰는 리눅스 서버다. AWS에는 같은 일을 하는 서비스가 둘 있다.

Lightsail EC2
요금 월 정액(1GB 약 $7, 공인 IPv4·트래픽 포함) 사용량에 따라 항목별 청구
설정 단순(방화벽, 고정 IP가 클릭 몇 번) 세밀(VPC, 보안 그룹, EBS 등)
맞는 곳 개인 프로젝트, 학습 회사 서비스, 큰 규모

예산이 정해져 있고 요금이 튀면 안 돼서 Lightsail을 골랐다. 사양: 1GB 메모리, 2 vCPU, 40GB SSD, Ubuntu 24.04 LTS, x86_64.

리전: 가까울수록 빠르다

리전 한국에서 왕복 시간(대략)
서울 ap-northeast-2 5~10ms
도쿄 ap-northeast-1 30ms
싱가포르 ap-southeast-1 70~80ms
시드니 ap-southeast-2 140~150ms

페이지 하나를 열 때 연결, TLS, HTML, CSS·JS로 여러 번 왕복하므로 지연이 4~6배로 쌓인다. 서울을 원했지만 무료 체험이 시드니로 정해져 있어 시드니로 시작했다. 나중에 옮기려면 스냅숏을 다른 리전으로 복사할 수 있지만, 고정 IP는 리전에 묶여 있어서 IP와 DNS를 바꿔야 한다.

Tip

ap-southeast-2(시드니)와 ap-northeast-2(서울)는 이름이 비슷해서 헷갈리기 쉽다. 만들기 전에 지도에서 위치를 확인한다.

자동 스냅숏은 끄고, 데이터는 S3로

자동 스냅숏은 매일 디스크 전체를 복사해 7개를 보관한다. 편하지만 저장 용량만큼 매달 요금이 붙는다. 이 블로그는 지켜야 할 데이터(DB, 업로드 이미지)만 매일 pg_dump와 압축으로 S3에 보내고(월 백 원 안쪽), 서버 설정은 명령 기록 파일로 남겨 다시 만들 수 있게 했다. 설정을 마친 직후 수동 스냅숏을 한 번 찍어 두는 것은 선택이다.

서비스가 늘면: 한 대를 키울까, 여러 대를 살까

한 대를 키움(스케일 업) 여러 대(스케일 아웃)
메모리 효율 OS, 프록시, DB를 한 번만 띄움 서버마다 또 띄움
관리 업데이트, 백업이 한 곳 대수만큼 반복
격리 한 서비스 폭주가 다른 서비스에 영향 서로 영향 없음

개인 서비스 몇 개라면 한 대를 키우는 쪽이 낫다. 프록시(Caddy) 하나가 서브도메인별로 나눠 보내고, PostgreSQL 하나에 서비스별 DB를 만든다. 컨테이너마다 mem_limit을 걸어 폭주를 가둔다. 남의 코드를 돌리거나 자원을 많이 쓰는 서비스라면 따로 둔다.

Lightsail은 실행 중인 서버 크기를 그 자리에서 못 바꾼다. 스냅숏 → 더 큰 플랜으로 새 인스턴스 → 고정 IP 옮기기 순서로 키운다. 고정 IP를 쓰는 이유다.

SSH 키, 고정 IP, 방화벽

  • SSH 키: 비밀번호 대신 키 파일로 접속한다. 개인키(.pem)는 내 PC에만, 공개키는 서버에. 무작위 대입이 사실상 불가능하다.
  • 고정 IP: 인스턴스를 껐다 켜도 주소가 유지된다. 인스턴스에 붙어 있는 동안 무료이고, 인스턴스를 지울 때 같이 해제해야 요금이 안 나온다.
  • 방화벽: 22(SSH), 80(HTTP), 443(HTTPS)만 연다. 앱(8080)과 DB(5432)는 Docker 내부 네트워크에만 있다.
ssh -i ~/.ssh/saip-lightsail.pem ubuntu@[고정 IP]

서버 기본 설정

sudo apt-get update && sudo DEBIAN_FRONTEND=noninteractive apt-get -y upgrade

# 스왑 2GB: 메모리가 순간적으로 모자라도 OOM Killer에 죽지 않고 느려지기만
sudo fallocate -l 2G /swapfile && sudo chmod 600 /swapfile && sudo mkswap /swapfile && sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
echo 'vm.swappiness=10' | sudo tee /etc/sysctl.d/99-swappiness.conf && sudo sysctl --system

sudo timedatectl set-timezone Asia/Seoul   # 서버는 시드니여도 cron 시각은 한국 기준

Docker는 우분투 기본 패키지가 아니라 Docker 공식 저장소에서 설치한다(최신 Engine과 compose 플러그인).

sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" \
  | sudo tee /etc/apt/sources.list.d/docker.list
sudo apt-get update && sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo usermod -aG docker ubuntu
  • 보안 업데이트 자동 설치(unattended-upgrades)는 우분투 클라우드 이미지에 기본으로 켜져 있다. cat /etc/apt/apt.conf.d/20auto-upgrades로 확인.
  • 첫 업그레이드에서 커널이 바뀌면 /var/run/reboot-required가 생긴다. 이때 재부팅한다.

처음 들여다본 서버 메모리는 909MB 중 OS가 이미 330MB를 쓰고 있었다. 1GB 서버에서 스왑이 필수인 이유다.

ghcr: GitHub의 Docker 이미지 창고

Java 세계 Docker 세계
jar 이미지
Maven Central Docker Hub, ghcr.io
mvn deploy docker push
의존성 받기 docker pull

이미지 이름은 ghcr.io/<아이디 소문자>/saip:<태그>다. 아이디에 대문자가 있어도 ghcr은 소문자만 받는다. 올릴 때는 gh의 인증을 빌려 쓰고 토큰은 출력하지 않는다.

gh auth refresh -h github.com -s write:packages    # 브라우저 승인 필요
gh auth token | docker login ghcr.io -u <아이디> --password-stdin
docker push ghcr.io/<아이디>/saip:latest

Dockerfile에 LABEL org.opencontainers.image.source=https://github.com/<아이디>/saip을 넣으면 패키지가 저장소에 연결된다. 패키지 공개 전환과 Actions 쓰기 권한은 패키지 설정 화면에서만 할 수 있다(API로 안 됨).

왜 코드와 이미지를 공개하나

  • 공개 저장소는 GitHub Actions가 무제한 무료이고, 공개 이미지는 저장 용량 제한이 없다.
  • 서버가 로그인 없이 docker pull만 하면 된다. 비공개면 서버에 읽기 토큰을 또 둬야 하고 만료되면 배포가 멈춘다.
  • 포트폴리오가 된다.
  • 비밀값을 코드 밖(서버 .env, GitHub Secrets)에 두었으니 공개해도 새로 드러나는 것이 없다.

Note

보안은 코드를 숨겨서 지키는 것이 아니라 비밀값을 코드 밖에 두는 것으로 지킨다.

공개 저장소에 처음 올리기 전에

공개 저장소는 모든 커밋 이력이 공개된다. 올리기 전에 세 가지를 했다.

  1. 이력 전체 비밀값 검사: 서버와 같은 정규식 파일로 git log --all -p의 추가된 줄을 검사했다. 걸린 것은 모두 테스트용 가짜 값(비밀번호 대입 예제 등)이었다.
  2. 커밋 이메일 숨기기: 커밋에는 작성자 이메일이 남아 누구나 볼 수 있다. 아직 push 전이라 이력을 다시 써서 GitHub 비공개 주소(<숫자>+<아이디>@users.noreply.github.com)로 바꿨다. 앞으로의 커밋도 git config user.email로 같은 주소를 쓴다.
git branch backup/before-email-rewrite      # 원래 이력은 로컬에만 보관(push 안 함)
git filter-branch --env-filter '
if [ "$GIT_AUTHOR_EMAIL" = "<옛 이메일>" ]; then export GIT_AUTHOR_EMAIL="<noreply 주소>"; fi
if [ "$GIT_COMMITTER_EMAIL" = "<옛 이메일>" ]; then export GIT_COMMITTER_EMAIL="<noreply 주소>"; fi' -- main
git diff --stat backup/before-email-rewrite main   # 비어 있으면 내용은 그대로
  1. GitHub Push Protection: 첫 push가 거절됐다. 민감 정보 검사기 테스트에 넣어 둔 가짜 Stripe 키를 GitHub이 진짜로 판단한 것이다. 허용 버튼을 누를 수도 있었지만, 아직 공개 전이라 이력에서 문자열을 둘로 쪼갰다("sk_live_" + "..."). 검사하는 값은 같아서 테스트는 그대로 통과한다.

Warning

이력 다시 쓰기는 아직 아무도 받아 가지 않은 저장소에서만 한다. 이미 공개된 뒤라면 키를 폐기하고 새로 발급하는 것이 먼저다. 이력에서 지워도 이미 복제된 사본은 남는다.

서버에 .env 만들기: 비밀값을 화면에 내지 않기

  • DB 비밀번호와 운영 API 토큰은 서버에서 openssl rand -hex로 만든다.
  • Google 클라이언트 값과 관리자 이메일은 PC의 사용자 환경변수에서 읽어 SSH 표준입력으로 서버 스크립트에 넘긴다(명령줄 인자는 프로세스 목록에 보인다).
  • 확인은 값 대신 KEY: set/empty만 출력하고, 권한은 chmod 600.
  • 운영 토큰은 서버에서 PC 환경변수로 바로 담아 오되 출력하지 않는다. 로컬 개발용 토큰과 운영 토큰은 다른 값으로 둔다.

로컬 노트를 서버로 옮기기

스키마는 앱이 시작할 때 Flyway가 만들므로 데이터만 옮긴다.

# 로컬(개발용 compose의 postgres 컨테이너)
docker exec <개발 DB 컨테이너> pg_dump -U blog --data-only --exclude-table=flyway_schema_history -Fc blog > blog-data.dump
scp blog-data.dump ubuntu@[고정 IP]:/tmp/
# 서버
docker compose exec -T db pg_restore -U blog -d blog --data-only < /tmp/blog-data.dump

데이터만 덤프해도 시퀀스 값이 같이 들어가서 새 노트를 만들 때 id가 겹치지 않는다. select last_value from note_id_seq와 max(id)를 비교해 확인했다.

겪은 문제와 해결

  • SSH 원격 명령 따옴표 지옥: 따옴표가 여러 겹으로 겹치다 깨졌다. 원격에서 돌릴 스크립트를 파일로 만들어 scp로 보내고 실행한 뒤 지우는 쪽이 안전하다.
  • PowerShell 파이프와 BOM·CRLF: PowerShell 5.1이 네이티브 프로그램으로 넘기는 문자열에 BOM이나 CRLF가 붙어 JSON 파싱이나 .env 값이 깨질 수 있다. 서버로 값을 넘길 때는 Git Bash를 쓰고 tr -d '\r'로 정리했다.
  • 패키지 비공개 상태에서 pull: 서버가 unauthorized를 받았다. 공개 전환 뒤 바로 성공.

댓글 0

로그인하고 댓글 달기