[Vapor/Swift] 프로젝트 생성부터 Synology NAS 배포까지: with 트러블슈팅
클라이언트 개발자들은 항상 서버, 백, API에 굶주리곤 합니다.
개인적으로 사용할 서비스에서 스케줄링 작업이 필요했습니다.
주 언어인 Swift, Swift에서 사용할 수 있는 웹 프레임워크 Vapor, 집에서 사용중인 NAS를 이용해서 배포한 과정을 공유합니다.
(Swift로 백엔드 개발하는 사람이 여기있네)
자세한 설치 방법은 공식문서를 참고해주세요.
전체적인 배포 흐름과 트러블슈팅을 중심으로 글이 작성되었습니다.
사용하는 프로젝트도 vapor 생성 시 준비되는 기본 todo 프로젝트입니다.
1. 프로젝트 시작하기
터미널에서 프로젝트를 생성할 디렉토리로 이동한 뒤 아래 명령어를 입력합니다.
vapor new sampleVapor
- 이후 ORM(Fluent) 사용이 필요한 경우 y를 입력하세요.
- Leaf가 필요한 경우 y를 입력하세요.
- 모든 옵션에 '아니오'로 답하고 싶다면
-n플래그를 붙이세요. (vapor new sampleVapor -n) - 저는 DB가 필요해서 ORM(Fluent), Postgres를 사용했습니다.
설치가 완료되면 Package.swift를 실행해 Xcode에서 프로젝트를 엽니다.
처음에는 의존성을 설치하느라 시간이 조금 필요할 수 있습니다.
실행 기기를 My Mac으로 선택하고 Run을 눌러봅시다.
2. 로컬 실행 중 마주친 문제들
Fatal error: Error raised at top level: bind(descriptor:ptr:bytes:): Address already in use) (errno: 48)
기존에 실행 중인 Vapor 프로세스가 포트(기본 8080)를 점유하고 있을 때 발생합니다.
저는 이미 기존에 사용 중인 프로젝트가 있어서 발생했습니다.
Edit Scheme -> Run -> Arguments에serve --port 8081을 추가하여 포트를 변경합니다.
No custom working directory set for this scheme
Vapor가 리소스 파일을 찾지 못할 때 발생합니다.
프로젝트가 로컬에서 실행될 때, 임시 폴더가 아닌 프로젝트 폴더에서 실행되도록 해야합니다.
public 폴더 내에 있는 공개 파일에 접근하는 경우에 필요합니다.
Edit Scheme -> Run -> Options에서 Use custom working directory를 체크하고 프로젝트 루트 폴더를 지정합니다.
3. Database 연동 및 마이그레이션
웹 브라우저에서 /todos 경로로 접속했을 때 role "유저네임" does not exist 에러가 난다면 DB 설정이 필요합니다.
vapor내에서 코드로 마이그레이션을 할 수도 있고, docker 이미지를 만들때 마이그레이션 옵션을 지정할 수도 있습니다.
로컬에서는 미리 데이터베이스를 생성해주고 유저 정보도 준비해줘야합니다.
DB 및 유저 생성 (PostgreSQL)
SQL
CREATE DATABASE vapor_database;
CREATE USER vapor_username;
ALTER DATABASE vapor_database OWNER TO vapor_username; -- 권한 부여
relation "todos" does not exist
테이블이 생성되지 않았을 때 발생합니다. Vapor는 서버 실행 시 자동으로 마이그레이션을 하지 않으므로 플래그를 추가해야 합니다.
- 실행 인자에 --auto-migrate를 추가합니다.
- vapor 프로젝트 내에서 코드로 오토 마이그레이션을 사용합니다.
permission denied for schema public
유저가 테이블 생성 권한이 없을 때 발생합니다. 위에서 언급한 ALTER DATABASE ... OWNER TO 명령어로 소유주를 변경하면 해결됩니다.
정상적으로 마이그레이션이 완료되면 콘솔에 [Migrator] Finished prepare 문구가 뜨며, /todos 접속 시 빈 배열([])을 확인할 수 있습니다.
4. Synology NAS 배포 (Docker 활용)
로컬에서 잘 돌아가는 프로젝트를 이제 나스로 옮겨봅시다.
1) 이미지 빌드 및 추출
나스는 보통 성능이 좋지 않기 때문에 코드를 올린 후 빌드하는 것보다 로컬에서 이미지를 만들어 올리는 게 빠릅니다.
# 1. 앱 이미지만 빌드
docker compose build app
# 2. 빌드된 이미지를 tar 파일로 저장
docker save -o samplevapor.tar samplevapor:latest
2) NAS 이미지 로드
- samplevapor.tar 파일을 시놀로지 File Station에 업로드합니다.
- Container Manager -> 이미지 -> 가져오기를 통해 해당 파일을 등록합니다.
- 레지스트리에서 postgres 이미지를 다운로드하여 버전을 맞춥니다.
3) 프로젝트 설정 및 트러블슈팅
Container Manager에서 프로젝트를 생성할 때 docker-compose.yaml을 사용합니다.
unable to prepare context... Dockerfile: no such file or directory
미 이미지가 있는데 컴포즈 파일에 build: 명령어가 남아있어 Dockerfile을 찾으려 함
- yaml 파일에서 build: 섹션을 삭제하고 image:만 남깁니다.
bind: address already in use (5432)
- 나스 자체 DB나 다른 컨테이너가 5432 포트를 이미 사용 중.
- 다른 포트를 지정해줍니다.
5. 외부 접속 설정
모든 컨테이너가 Exit code: 0 없이 정상 실행되었다면, 외부에서 접속할 차례입니다.
- 공유기 설정(Port Forwarding)에서 외부 포트(예: 8081)를 나스 내부 IP의 8080으로 연결합니다.
- 브라우저에서 http://외부아이피:8081 접속 시 Hello Vapor!가 보이면 성공입니다.
6. 그 밖의 에러들
Connection request timed out. This might indicate a connection deadlock...
AsyncKit.ConnectionPoolTimeoutError.connectionRequestTimeout
앱이 켜지자마자 DB 연결에서 타임아웃이 발생하면서 에러가 발생했습니다.
여러가지 방법으로 문제 해결을 시도했습니다.
- Healthcheck 사용
- DB 서비스 쪽에 pg_isready를 이용한 healthcheck를 넣고, 앱 서비스 쪽에 depends_on: condition: service_healthy를 넣었습니다.
- netcat 사용
- netcat을 사용해서 DB 연결을 직접 확인하고 연결될 때 까지 지연을 시켰습니다.
- Dockerfile 내에 apt-get install -y netcat-openbsd를 추가하고, wait-for-db.sh라는 파일을 도커 이미지 안에 넣었습니다.
7. .env 파일을 사용
NAS 디렉토리에 .env 파일을 넣었고 컴포즈파일에서도 변수를 잘 읽어오는데, 정작 앱 서버에서는 변수를 읽어오지 못했습니다.
services:
app:
# 기존 설정들...
env_file:
- .env # (docker-compose.yml과 같은 폴더에 .env 파일이 있어야 합니다)
컴포즈 파일에서 env_file 옵션을 app에 추가해줍니다.
8. 계속 되는 타임아웃
코드, 아이피, 타임아웃 설정을 수정했는데도 계속 연결이 되지 않는 상황이 있었습니다.
Vapor 코드 내에서 DB 환경설정을 할때 tls: .prefer(try .init(configuration: .clientDefault)) 코드가 있었는데 기본 로컬 도커, postgresql18 상황에서 별도의 세팅이 준비되어있지 않아서 문제가 있었습니다.
tls: .disable로 수정해서 보안 연결을 해제했습니다.
단순히 서버 하나를 띄우는 것이었으나, NAS 환경은 로컬, 클라우드 환경과 달라서 예상치 못한 삽질이 많았습니다.