no image
Oracle Cloud 인스턴스 생성
Oracle Cloud 사용하는 이유?1. 무료 서버의 기간 제한이 없다. AWS는 프리티어 기간이 1년.GCP(Google Cloud Platform)의 경우, 300달러로 넉넉하지만 90일의 제한 기간이 있음.반면, 오라클 클라우드는 무료로 제공되는 VM의 제한기간이 없어 계속해서 서버를 띄워둘 수 있다!! 2. VM을 2대나 지원한다. AWS와 유사한 스펙의 무료 서버를 2대나 지원한다.여러 개의 프로젝트를 띄우려는 나에게는 2대를 이용해 띄우는게 훨씬 안정적이고 부담이 덜하다.또한, 실 서비스용 서버를 만든다고 해도, 하나는 서버용 하나는 DB용으로 나눠 더 안정적 운영이 가능하다. 회원가입우선 회원가입 먼저 한다.나는 리전을 싱가포르로 선택함 (하지만 이 글을 보는 사람들은 다른 곳을 선택..
2025.08.03
백준 1912: 연속합[Silver 2] / Java
백준 1912: 연속합https://www.acmicpc.net/problem/1912문제 설명N개의 정수로 이루어진 수열이 주어졌을 때, 연속된 몇 개의 수를 선택해서 구할 수 있는 합 중 최대값을 구하는 문제다.단, 수는 한 개 이상 선택해야 한다.1차 시도 (실패)이 문제를 마주했을 때, 가장 먼저 생각난 건 누적합을 활용한 방식이었다.모든 수열을 순회하면서 sum[앞의값] - sum[뒤의값]으로 계산할 수 있으니,누적합 배열을 만들고각 위치마다 그 전까지의 최소 누적합을 빼면 최대 구간합이 되지 않을까? 라고 생각했다.나보다 앞선 누적합 중 가장 작은 값을 찾으려면, 누적합 배열을 인덱스와 함께 정렬해서 탐색하면 될 것 같았다.이렇게 작성한 코드는 다음과 같았다:for (int i = 0; i ..
2025.08.02
no image
[싸피 후기] SSAFY 12기 수료 후기 (feat. 고용노동부 장관상 수상!!)
벌써 1년이 지나 수료 후기를 쓰는 날이 오네요. 1년 동안 나름 열심히 공부하고, 매달린 끝에 아래와 같이 여러 성과를 거두었습니다. ✨고용노동부 장관상 (!!!) ✨ 2학기 특화프로젝트 우수상 (3위) 1학기 종합 성적 우수상 (1위) 1학기 관통 프로젝트 우수상 (2위) + 수료증, 이수증 장관상까지 받아 유종의 미를 거뒀지만, 그럼에도 아쉬웠던 점과 좋았던 점, 그리고 그 밖의 제 생각을 정리하고자 수료 후기를 남겨보려 합니다. 왜 SSAFY에 들어갔나요?전 비전공자였고, 개발자로 직종을 변경하기 위해 개발자 관련 책도 읽어보고, 인터넷도 많이 뒤져봤습니다. 보통 국비교육, 부트캠프, 인강 3가지 루트가 있더군요. 부트캠프를 가장 많이 가는 추세였고, 저도 좀 더 깊이 배..
2025.07.25
no image
[트러블슈팅 - Nginx] cannot load certificate "/etc/letsencrypt/live/www.film-moa.com/fullchain.pem": BIO_new_file() failed...
에러 메시지 / 문제 상황Docker Compose로 Nginx 컨테이너 실행 시 다음과 같은 오류 발생함.cannot load certificate "/etc/letsencrypt/live/www.film-moa.com/fullchain.pem": BIO_new_file() failed...초기에는 페이지가 제대로 렌더링되지 않고 Nginx 기본 화면만 표시되거나 컨테이너가 반복적으로 종료되는 현상 발생함. 원인최종 원인은 Nginx 컨테이너가 SSL 인증서 파일에 접근할 권한이 없어서 인증서를 불러오는 데 실패했던 것이었음실제로 MobaXterm을 통해 파일 접근했을 때, /etc/letsencrypt까지는 접근이 되나, 이후 /live, /archive 디렉토리에는 접근이 불가했음 (Permis..
2025.06.05
no image
[트러블슈팅 - Docker] E: List directory /var/lib/apt/lists/partial is missing. - Acquire (13: Permission denied)
에러메세지 / 문제상황컨테이너 내에서 명령어 실행 시 apt-get update 실행 시 다음과 같은 권한 에러 발생: 원인 컨테이너가 jenkins 사용자 권한으로 실행되고 있었으며, 해당 사용자는 apt 명령 실행에 필요한 시스템 디렉토리에 대한 쓰기 권한이 없음. jenkins 이미지는 기본적으로 루트가 아닌 사용자로 동작함. 시도 1services: jenkins: image: jenkins/jenkins:lts ports: - "8080:8080" volumes: - jenkins_home:/var/jenkins_home - /var/run/docker.sock:/var/run/docker.sock restart: unless-stopped→..
2025.06.04
[트러블슈팅 - Docker / Jenkins / SpringBoot] Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured. Reason: Failed to determine suitable jdbc url
에러메세지 / 문제상황Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured.Reason: Failed to determine suitable jdbc url 백엔드 개발자들은 많이 보는 에러일 것이다.springboot 서버를 실행할 때, 설정파일 (application.yaml)을 제대로 못읽었을 때 발생하는 에러이다.. 원인Spring Boot 프로젝트에서 데이터베이스 연결 정보를 application.yml과 application-secret.yml 두 개 파일로 나눠 관리 중이었음. Jenkins를 이용해 application-secret.yml을 ..
2025.06.03
no image
[트러블슈팅 - Docker / Next.js] Module not found: Can't resolve '@/app/components/feed/FeedList’ Module not found: Can't resolve '@/components/Menubar’
에러메세지/문제상황Docker 환경에서 Next.js 애플리케이션을 빌드할 때 다음과 같은 에러가 발생함:Module not found: Can't resolve '@/app/components/feed/FeedList'Module not found: Can't resolve '@/components/Menubar'해당 파일은 실제로 존재함에도 불구하고 Next.js 빌드 중 모듈을 찾을 수 없다는 오류가 발생함로컬에서는 정상 동작하나, Docker에서만 발생하는 점이 주요 특징이었음원인1차 원인초기 프로젝트 구조가 다음과 같았음:FRONTEND/ ├── src/ │ └── app/ │ └── components/ │ └── feed/ │ ..
2025.06.01
no image
[트러블슈팅 - Docker] 포트 범위 문제 8080-8081/tcp
에러메세지 / 문제상황docker ps 명령어 결과에서 spring-container가 8080-8081/tcp 식으로 포트 범위를 열고 있는 것이 확인됨.nginx를 통한 API 요청 시 502 Bad Gateway 오류 발생.원인Dockerfile과 docker-compose.yml의 포트 설정 불일치.Dockerfile에서는 EXPOSE 8081로 되어 있었고,docker-compose.yml에서는 expose: 8080, environment: SPRING_CONTAINER_PORT=8080으로 설정되어 있었음.Spring Boot 서버는 8080 포트에서 동작했지만, Docker 메타데이터(EXPOSE)에는 8081이 등록되어 있었기 때문에 docker ps 출력에서 8080-8081 포트 범위..
2025.05.31
no image
[트러블슈팅 - Docker] mysqld: Cannot change permissions of the file 'ca.pem' (OS errno 1 - Operation not permitted)[ERROR] Could not set file permission for ca.pem[ERROR] The designated data directory /var/lib/mysql/ is unusable.
에러메세지 / 문제상황MySQL Docker 컨테이너 실행 직후 자동 종료됨. docker logs 결과:mysqld: Cannot change permissions of the file 'ca.pem' (OS errno 1 - Operation not permitted)[ERROR] Could not set file permission for ca.pem[ERROR] The designated data directory /var/lib/mysql/ is unusable. 원인MySQL은 실행될 때 데이터를 저장할 공간(폴더)이 필요함.그 저장 공간을 Windows 컴퓨터 안의 폴더(예: C드라이브 경로)로 연결했고, MySQL은 해당 폴더 안에서 파일 권한을 바꾸거나 소유자를 설정하려고 함.그러나 W..
2025.05.30
no image
[트러블슈팅 - Docker] Error response from daemon: Ports are not available: exposing port TCP 0.0.0.0:3306 -> 127.0.0.1:0: listen tcp 0.0.0.0:3306: bind: Only one usage of each socket address (protocol/network address/port) is normally permitted.
에러메세지 / 문제상황Docker 컨테이너(MySQL)를 실행하려 하자 다음과 같은 에러 발생:Error response from daemon: Ports are not available: exposing port TCP 0.0.0.0:3306 → 127.0.0.1:0: listen tcp 0.0.0.0:3306: bind: Only one usage of each socket address is normally permitted.→ docker start 또는 docker run 실행 시 3306 포트 충돌로 인해 컨테이너 시작 실패 원인Windows에서이미 다른 프로세스가 3306 포트를 선점 중이어서 Docker가 동일 포트에 바인딩하지 못함.(Docker는 기본적으로 MySQL 컨테이너에 330..
2025.05.29

Oracle Cloud 인스턴스 생성

친환경 개발자
|2025. 8. 3. 08:15

Oracle Cloud 사용하는 이유?

1. 무료 서버의 기간 제한이 없다.

 

AWS는 프리티어 기간이 1년.

GCP(Google Cloud Platform)의 경우, 300달러로 넉넉하지만 90일의 제한 기간이 있음.

반면, 오라클 클라우드는 무료로 제공되는 VM의 제한기간이 없어 계속해서 서버를 띄워둘 수 있다!!

 

2. VM을 2대나 지원한다.

 

AWS와 유사한 스펙의 무료 서버를 2대나 지원한다.

여러 개의 프로젝트를 띄우려는 나에게는 2대를 이용해 띄우는게 훨씬 안정적이고 부담이 덜하다.

또한, 실 서비스용 서버를 만든다고 해도, 하나는 서버용 하나는 DB용으로 나눠 더 안정적 운영이 가능하다.

 

 

 

회원가입

우선 회원가입 먼저 한다.

  • 나는 리전을 싱가포르로 선택함  (하지만 이 글을 보는 사람들은 다른 곳을 선택하기를.. 가용 자원이 없을 확률이 있습니다.)
    • 서울이나 일본은 인스턴스를 1개밖에 생성 못할 수도 있다고 하여 다른 나라를 선택해야 했음.
    • https://www.cloudping.info/
    • 리전별 속도를 확인할 수 있는 사이트인데, 다른 지역 보다 싱가포르가 잘 나오는 편이여서 선택.

 

 

VCN 생성

 

인스턴스 생성 전, vcn 먼저 생성해줘야 한다. 

인스턴스 생성 단계에서 VCN을 선택하는 란이 있는데, 그 때가서 만들면 꼬이기 때문.

Oracle Cloud Console 접속 → 좌상단 메뉴 → Networking → Virtual cloud network → Create VCN

 

 

이름 설정 및 구획 선택.

컴파트먼트는 따로 건들지 않았다면 root가 기본으로 설정됨.

 

 

이후 Create 클릭하면

생성 완료!

 

ssh 키 생성

인스턴스 만들기 전, ssh키도 미리 생성해줘야 한다.

인스턴스 만들 때 ssh키 등록하는 부분이 있기 때문이다.

MobaXterm 으로 생성할 것이다.

tools → MobaKeyGen 클릭

 

 

 

 

Generate 클릭

 

  1. 메모장에 ssh-rsa ~~~ 부분 모두 드래그해서 복사한 뒤, .pub 확장자로 저장

  2. save private key 버튼 눌러 .ppk 저장

잃어버리지 않게 저장 완료 후 폴더 위치를 잘 기억해둘 것.

 

 

인스턴스 만들기

VM 이름을 입력하고, 컴파트먼트도 선택한다.

 

  • 이미지
    • ubuntu 선택 ⇒ Canonical ubuntu 22.04 선택 (Arm용)
  • 셰입
    • A1.Flex 선택

 

 

그리고, 

2번의 보안 설정은 우선 기본값으로 두고 넘어갔다. 추후 생성 후에 조정할 수 있다.

 

 

3번 네트워킹 설정

vnc 이름 설정 후

아까 미리 만들어둔 vcn과 서브넷을 선택한다.

ipv4도 할당되도록 선택할 것.

 

 

 

 

4번 Storage

부트볼륨은 OS인 우분투가 설치되는 디스크.

기본 설정에서 건드리지 않고 넘어감.

 

 

이후 Create 버튼을 눌러 생성하면 완료!!

 

백준 1912: 연속합

https://www.acmicpc.net/problem/1912


문제 설명

N개의 정수로 이루어진 수열이 주어졌을 때, 연속된 몇 개의 수를 선택해서 구할 수 있는 합 중 최대값을 구하는 문제다.단, 수는 한 개 이상 선택해야 한다.


1차 시도 (실패)

이 문제를 마주했을 때, 가장 먼저 생각난 건 누적합을 활용한 방식이었다.

모든 수열을 순회하면서 

sum[앞의값] - sum[뒤의값]
으로 계산할 수 있으니,

  1. 누적합 배열을 만들고
  2. 각 위치마다 그 전까지의 최소 누적합을 빼면 최대 구간합이 되지 않을까? 라고 생각했다.
  3. 나보다 앞선 누적합 중 가장 작은 값을 찾으려면, 누적합 배열을 인덱스와 함께 정렬해서 탐색하면 될 것 같았다.

이렇게 작성한 코드는 다음과 같았다:

for (int i = 0; i < N; i++) {
    int max = sum[i];
    for (int j = i + 1; j < N; j++) {
        int tmp = sum[j] - sum[i];
        if (tmp > max) {
            max = tmp;
        }
    }
    ans = Math.max(max, ans);
}
 

이중 반복문을 통해 각 구간합을 계산하고, 최댓값을 갱신하는 구조다.

 

 

결과는 시간 초과가 발생했다.

생각해보니 이중 for문 구조는 O(N²) 의 시간복잡도였고,입력값이 최대 100,000이기 때문에 이 방식은 실질적으로 불가능했다.

나는 처음에 "정렬 없이 그냥 순회하면서 빼는 거니까 O(N log N) 아닌가?"라고 착각했지만,실제로는 모든 (i, j) 쌍에 대해 sum[j] - sum[i]를 계산하기 때문에 완전한 이중 순회로 O(N²)이다.정렬을 포함했더라도 O(N log N)이 아닌, 여전히 느린 구조였을 것이다.


2차 시도

결국 다시 인터넷을 뒤져보며 동적 계획법을 활용해야함을 깨달았다.

이 알고리즘은 현재 위치에서

  • 지금까지의 누적 최대값에 현재 수를 더하는 것과
  • 현재 수 하나만 사용하는 것
    중에서 더 큰 값을 고른다.

즉, 아래와 같은 점화식이다:

dp[i] = max(arr[i], dp[i-1] + arr[i])

 

for (int i = 0; i < N; i++) {
    int max = sum[i];
    for (int j = i + 1; j < N; j++) {
        int tmp = sum[j] - sum[i];
        if (tmp > max) {
            max = tmp;
        }
    }
    ans = Math.max(max, ans);
}

이 코드는 O(N) 으로 매우 빠르며, 메모리도 효율적이다.


회고

  • 처음 시도한 방식처럼, 누적합과 정렬을 이용해 푸는 방식도 사용되긴 한다.
    예를 들어 "구간합이 K 이상인 최소 길이 구간" 같은 문제에서는 유용하다.
    하지만 이 문제처럼 단순한 연속 구간합의 최대값을 구하는 문제에는 오히려 복잡하고 비효율적이다.
  • 이 문제는 전형적인 동적프로그래밍으로 푸는 문제였다. 그런데 풀이법이 잘 생각나지 않았다.
  • 동적프로그래밍 문제를 많이 풀어봤다 생각했는데, 더 다양하게 풀어봐야겠다.

 

 

벌써 1년이 지나 수료 후기를 쓰는 날이 오네요.

 

 

 

1년 동안 나름 열심히 공부하고, 매달린 끝에 아래와 같이 여러 성과를 거두었습니다.

 

 

 

✨고용노동부 장관상 (!!!) ✨

 

2학기 특화프로젝트 우수상 (3위)

 

1학기 종합 성적 우수상 (1위)

 

1학기 관통 프로젝트 우수상 (2위)

 

+ 수료증, 이수증

 

 

 

장관상까지 받아 유종의 미를 거뒀지만,

 

 

 

그럼에도 아쉬웠던 점과 좋았던 점, 그리고 그 밖의 제 생각을 정리하고자 수료 후기를 남겨보려 합니다.

 

 

 

 

 

 

 

왜 SSAFY에 들어갔나요?

전 비전공자였고, 개발자로 직종을 변경하기 위해 개발자 관련 책도 읽어보고, 인터넷도 많이 뒤져봤습니다.

 

보통 국비교육, 부트캠프, 인강 3가지 루트가 있더군요. 부트캠프를 가장 많이 가는 추세였고, 저도 좀 더 깊이 배우고, 프로젝트도 경험할 수 있는 부트캠프를 가야겠다는 생각을 했습니다.

 

그러던 중 지인이 싸피의 존재에 대해 알려주었고, 찾아보니 이름 있는 부트캠프 중에서 개발 경험이 없는 비전공자가 들어가기에 가장 가능성이 높다고 생각했습니다. 그리고 그 이전에, 아래 3가지 장점이 제게 가장 큰 매력으로 느껴졌습니다.

 

 

1. 다른 부트캠프에 비해 1년 커리큘럼으로 개발 공부도 더 깊게, 프로젝트 경험도 많이 쌓을 수 있다.

 

2. 교육지원금을 받으며 다닐 수 있다.

 

3. 자취를 함으로써 공부에 집중할 수 있는 환경을 만들 수 있다.

 

 

 

 

그렇다면 1년 뒤 지금은 어떤가요?

제가 기대했던 대로 1학기 동안엔 알고리즘에 대해 배우고, 제 트랙이었던 Java, Spring, DB 등 개발 기술을 체계적으로 배울 수 있어 만족스러웠습니다. 1학기에 배워둔 알고리즘은 2학기에도 계속 매일 1문제씩은 꼭 푸는 것을 목표로 실천해왔고, github에 기록해뒀습니다. (잔디는 덤!)

개발 기술도 실습 예제가 정말 잘 마련돼있어서 그날 배운 내용을 실습을 통해 제 것으로 만들 수 있었습니다. 이 부분은 들어오기 전에는 잘 몰랐는데, 정말 도움을 많이 받은 부분이었습니다.

 

2학기에는 3개의 프로젝트를 진행하는데, 컨설턴트님과 실습코치님이 반마다 배정되어 모르는 것을 언제든지 가서 문의할 수 있었습니다. 주기적으로 미팅도 하도록 되어 있어 많은 조언과 인사이트를 얻을 수 있어 좋았습니다.

또 협업이라는 경험도 정말 값진 경험인 것 같습니다. 혼자 프로젝트를 하는 것과 여럿이서 하는 것은 정말 차이가 크다고 생각합니다. 프로젝트를 하면서 정말(x10) 힘들었지만, 그만큼 많이 배웠습니다.

힘들었던 이유는, 프로젝트에 온 시간을 다 바치면서 피로가 많이 쌓이기도 했고, 혼자 하는 것이 아니다보니 제 생각과 다르게 프로젝트 방향이 진행될 때도 있고 이를 조율하는 과정에서도 많은 고민을 하다보니 힘들었습니다. 그리고 결국 취업을 위한 교육과정이다 보니 취업 준비를 우선적으로 하는 팀원들이 항상 존재하는데, 어떻게 하면 이들과 함께 프로젝트를 잘 이끌어나갈지 고민하는 과정도 힘들었습니다.

2학기를 앞둔 분들이 만약 이 글을 보신다면.. 파이팅! 결국엔 누구나 해내게 되어있습니다. 그러니까 걱정 마시고 지금 그 순간에 집중하셨으면 좋겠습니다.

 

 

어떻게 공부했나요?

1학기에는 기본기를 다지는 기간입니다.

매일 수업이 진행되는데요, 수업 후에는 준비된 실습자료로 직접 코드를 작성해볼 수 있습니다.

당연한 얘기지만, 절대 머리로만 이해하지 말고 직접 코드 쳐보면서 내 것으로 만들어야 합니다. 저는 그날 배운 내용은 그날 다 소화하고 간다는 철칙을 세우고 지켰습니다. 생각보다 진도가 빠르기 때문에, 놓치면 순식간에 지나가 뒷부분까지 이해를 못할 수도 있어서 꼭 진도를 따라가야 했습니다!

 

1학기가 끝나고 방학 때에는, 프로젝트를 대비해 JPA나 React를 공부할까? 아니면 개인적인 자격증 공부를 할까? 고민했었는데요,

결론적으로는 개인적인 것들을 최대한 만들어두고, 프로젝트에 쓰일 기술 공부는 프로젝트를 진행하면서 하는 것이 좋다고 판단해 오픽 공부를 했습니다! 실제로도 프로젝트를 하면서 새로운 기술을 많이 접하는데, 어떻게든 구현은 할 수 있기 때문에 프로젝트 때는 프로젝트 관련된 것만 최대한 집중하고 방학 때 개인적 자격증 공부를 하는게 팀원들에게 미안할 일도 없고 좋았던 것 같습니다.

 

2학기에는 프로젝트를 하면서 새로운 기술스택을 사용할 때마다 그 기술을 공부했습니다.

인프런이나 유튜브 강의를 들으며 기본기를 어느정도 익히고, 세부적으로 들어갈 때는 GPT + 구글링을 많이 활용하였습니다.

GPT가 빠르게 구현하기엔 좋지만, 실제로 잘 쓰이지 않는 방식으로 구현하거나 잘못된 구조로 구현하는 경우도 많기 때문에 기본기는 최대한 인강 같은 자료를 많이 활용했습니다.

저는 2개의 프로젝트에서는 백엔드, 마지막 프로젝트에서는 Infra 를 맡았었는데요.

백엔드에서는 JPA, SpringSecurity 를 새롭게 공부할 때 인프런 김영한 선생님의 강의와 유튜브 '개발자 유미' 강의를 들으며 기초를 다졌습니다. Infra 파트에서는 Docker에 대한 10시간 짜리 짧은 강의를 빠르게 수강했고, 컨테이너를 혼자 띄워보며 어느정도 친숙해진 이후, Jenkins와 Nginx같은 것들은 주변 동료들과 컨설턴트님을 최대한 활용했습니다 ㅎㅎ

 

 

 

 

 

쓰다보니 길어졌네요, 나머지 2학기에 대한 내용은 추후에 이어서 작성해보겠습니다.

에러 메시지 / 문제 상황

Docker Compose로 Nginx 컨테이너 실행 시 다음과 같은 오류 발생함.

cannot load certificate "/etc/letsencrypt/live/www.film-moa.com/fullchain.pem": BIO_new_file() failed...

초기에는 페이지가 제대로 렌더링되지 않고 Nginx 기본 화면만 표시되거나 컨테이너가 반복적으로 종료되는 현상 발생함.

 

 


 

원인

  • 최종 원인은 Nginx 컨테이너가 SSL 인증서 파일에 접근할 권한이 없어서 인증서를 불러오는 데 실패했던 것이었음
  • 실제로 MobaXterm을 통해 파일 접근했을 때, /etc/letsencrypt까지는 접근이 되나, 이후 /live, /archive 디렉토리에는 접근이 불가했음 (Permission denied 에러)
  • 호스트에서 /etc/letsencrypt/live/... 경로의 인증서 파일들은 심볼릭 링크로 구성되어 있는데,
  • 실제 인증서 파일이 있는 /etc/letsencrypt/archive/... 경로에 대한 권한이 컨테이너 내부에서 부족했기 때문임.

 


 

시도

시도 1

인증서 파일명에 '1'이 붙은 문제를 의심하여 nginx.conf 수정

  • 중간에 certbot문제라고 생각하여 인증서를 재설치했었음 
  • sudo certbot --nginx -d www.film-moa.com -d film-moa.com

  • /etc/letsencrypt/archive 폴더를 ls -l 명령어로 확인 시 파일명 뒤에 1이 붙어 있는 것을 확인하여 nginx.conf파일에 파일명을 수정함

변경전

  # 2. HTTPS(443포트) 요청을 처리하는 서버 블록
  server {
    listen 443 ssl;
    server_name www.film-moa.com film-moa.com;

    ssl_certificate /etc/letsencrypt/live/www.film-moa.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/www.film-moa.com/privkey.pem;

    ... 중략 ...
  }

 

변경후

  # 2. HTTPS(443포트) 요청을 처리하는 서버 블록
  server {
    listen 443 ssl;
    server_name www.film-moa.com film-moa.com;

    ssl_certificate /etc/letsencrypt/live/www.film-moa.com/fullchain1.pem;
    ssl_certificate_key /etc/letsencrypt/live/www.film-moa.com/privkey1.pem;

    ... 중략 ...
  }

⇒ 그러나 여전히 같은 에러가 발생 BIO_new_file() failed

시도 2

시스템 nginx 프로세스 점유 의심 및 중지

  • docker compose down 후 다시 컨테이너를 띄우고 log를 확인하니, 80포트가 이미 사용 중으로 나옴
  • 포트 충돌 문제라고 판단해 프로세스를 강제 종료함

⇒ 그러나 여전히 같은 에러가 발생 BIO_new_file() failed

시도 3

심볼릭 링크 구조 및 실제 파일 존재하는지 접근하여 확인 & 권한 수정

  • ls -l /etc/letsencrypt/live/www.film-moa.com/ 명령어로 심볼릭 링크 확인함.
  • ls -l /etc/letsencrypt/archive/www.film-moa.com/ 명령어로 실제 타겟 파일 존재 여부 확인함.
  • 그러나 일반 사용자(ubuntu)로 접근 시 권한 에러(Permission denied) 발생하는 것을 확인함.
  • 컨테이너 내부에서 root가 아닌 별도의 사용자 계정으로 인증서를 읽으려 했기 때문에 문제가 발생했음을 최종적으로 확인함.
  • 해당 경로에 대해 권한을 수정함
sudo chmod 755 /etc/letsencrypt
sudo chmod 755 /etc/letsencrypt/live
sudo chmod 755 /etc/letsencrypt/archive
sudo chmod 755 /etc/letsencrypt/live/www.film-moa.com
sudo chmod 755 /etc/letsencrypt/archive/www.film-moa.com
  • 755
  • 소유자(owner)는 읽기(r), 쓰기(w), 실행(x) 권한을 모두 갖고, 다른 사용자(group, others)는 읽기(r), 실행(x) 권한만 가지는 설정임.

해결책

컨테이너가 호스트의 /etc/letsencrypt 경로를 마운트하여 내부에서 접근할 때 권한이 충분한지 반드시 확인해야 함.

  • 해결책으로 호스트에서 인증서가 있는 디렉터리(/etc/letsencrypt) 권한을 컨테이너에서 접근 가능한 형태로 변경하거나,
  • 호스트에서 실제 인증서 파일을 복사하여 별도의 디렉토리에 배치하고, 이 디렉토리를 Docker 볼륨으로 마운트하는 방법으로 권한 문제 해결이 가능함.
  • 본 프로젝트에서는 /etc/letsencrypt 경로의 접근 권한을 컨테이너가 읽을 수 있도록 수정하여 문제 해결함.

회고 및 정리

  • 문제 발생 초기에 Nginx 설정 파일이나 포트 충돌 문제 등 명확한 문제를 찾는 데 많은 시간을 소모하였음.
  • 여러 시도를 통해 문제를 좁혀갔으나, SSL 인증서 파일의 접근 권한 문제라는 근본적인 문제를 인지하는 데 시간이 오래 걸림.
  • 앞으로 SSL과 같이 권한 설정이 중요한 부분을 다룰 때는, 처음부터 권한 문제를 염두에 두고 접근 권한 및 파일 소유자, 그룹 등의 설정을 우선적으로 확인할 필요가 있음을 깨달았음.
  • 이를 통해 Docker 컨테이너와 호스트 간 볼륨 마운트 시 발생할 수 있는 권한 문제에 대한 이해도를 높였으며, 향후 유사 문제 발생 시 더 빠른 원인 진단이 가능할 것으로 기대함.

에러메세지 / 문제상황

컨테이너 내에서 명령어 실행 시 apt-get update 실행 시 다음과 같은 권한 에러 발생:

 


 

원인

 

컨테이너가 jenkins 사용자 권한으로 실행되고 있었으며, 해당 사용자는 apt 명령 실행에 필요한 시스템 디렉토리에 대한 쓰기 권한이 없음. jenkins 이미지는 기본적으로 루트가 아닌 사용자로 동작함.

 

 

시도 1

services:
  jenkins:
    image: jenkins/jenkins:lts
    ports:
      - "8080:8080"
    volumes:
      - jenkins_home:/var/jenkins_home
      - /var/run/docker.sock:/var/run/docker.sock
    restart: unless-stopped

→ Docker 소켓 공유는 있었으나, root 권한 설정이 없어 apt 실행 불가.

시도 2

컨테이너 내부(exec)에서 강제 수행을 위해 sudo apt-get update 실행 시도.

결과: sudo: command not found.

기본 이미지에 sudo가 포함되지 않음.

 

시도 3

services:
  jenkins:
    image: jenkins/jenkins:lts
    user: jenkins
    group_add:
      - "${DOCKER_GID}"
    ports:
      - "8080:8080"
    volumes:
      - jenkins_home:/var/jenkins_home
      - /var/run/docker.sock:/var/run/docker.sock
    restart: unless-stopped

→ 보안 목적상 jenkins 유저로 제한하고 group_add 설정을 통해 Docker 소켓 접근 권한을 주었으나, 여전히 apt, groupadd, usermod 실행 불가.

 

시도 4

services:
  jenkins:
    image: jenkins/jenkins:lts
    user: root
    group_add:
      - "${DOCKER_GID}"
    ports:
      - "8080:8080"
    volumes:
      - jenkins_home:/var/jenkins_home
      - /var/run/docker.sock:/var/run/docker.sock
    restart: unless-stopped

→ 컨테이너를 root 사용자로 실행하여 apt 관련 시스템 명령이 정상 작동함.

 

 


 

해결책

docker-compose.yml에 user: root 옵션 추가.

컨테이너를 루트 사용자 권한으로 실행함으로써 apt, groupadd, usermod 등 시스템 명령 실행 가능해짐.

Jenkins는 여전히 컨테이너 내에서 자체적으로 jenkins 사용자로 실행되므로 보안상 큰 문제 없음

 

 


 

회고 및 정리

Jenkins 컨테이너에서 시스템 명령어(apt, groupadd 등)를 실행하기 위해 user: root 설정을 적용하였다.

일반적으로 Docker 컨테이너는 사용자 공간이 호스트와 격리되어 있어, 컨테이너 내 root 권한은 호스트 시스템의 보안에 직접적인 위협이 되지 않는다.

 

따라서 컨테이너 내부에서 root로 실행해도 시스템 자원을 침해하지 않으며, 제한된 범위 내에서 필요한 작업만 수행할 수 있다.

또한 Jenkins 공식 Docker 이미지는 내부적으로 Jenkins 프로세스를 별도의 일반 사용자(jenkins)로 실행하는 구조이기 때문에

컨테이너가 root로 시작되더라도 Jenkins 서비스는 최소 권한 환경에서 운영된다.

 

이로 인해 시스템 도구 설치나 Docker CLI 접근과 같은 자동화 작업은 루트 권한으로 처리하면서도,

Jenkins 자체는 보안 측면에서 안전하게 동작할 수 있다.

 

결론적으로, 이 구성은 Docker 환경 내에서 실용성과 보안을 모두 고려한 현실적인 운영 방식이며,

user: root 설정은 Jenkins 컨테이너 운영에 있어 유효하고 안정적인 선택이었다.

에러메세지 / 문제상황

Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured.
Reason: Failed to determine suitable jdbc url

 

백엔드 개발자들은 많이 보는 에러일 것이다.

springboot 서버를 실행할 때, 설정파일 (application.yaml)을 제대로 못읽었을 때 발생하는 에러이다..

 

원인

Spring Boot 프로젝트에서 데이터베이스 연결 정보를 application.yml과 application-secret.yml 두 개 파일로 나눠 관리 중이었음. Jenkins를 이용해 application-secret.yml을 credentials에서 복사해 프로젝트 경로에 넣어줬으나, Spring Boot가 실제 실행 시 ${}로 선언된 환경변수를 JVM 또는 컨테이너 환경에서 찾지 못해 데이터베이스 연결 정보가 누락되어 발생한 문제.

application.yml (기존)

server:
  servlet:
    context-path: /api/v1
  port: ${SPRING_CONTAINER_PORT}

spring:
  application:
    name: fourcut
  config:
    import: "optional:classpath:application-secret.yml"
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: ${DB_URL}
    username: ${DB_USERNAME}
    password: ${DB_PASSWORD}
  jpa:
    database-platform: org.hibernate.dialect.MySQLDialect
    hibernate:
      ddl-auto: update  # none, update, create, create-drop
    properties:
      hibernate:
        show_sql: true
        format_sql: true
        use_sql_comments: true
        dialect: org.hibernate.dialect.MySQLDialect

  security:
    oauth2:
      client:
        registration:
          kakao:
            client-id: ${KAKAO_CLIENT_ID}
            client-secret: ${KAKAO_CLIENT_SECRET}
            redirect-uri: ""
            authorization-grant-type: authorization_code
            client-authentication-method: client_secret_post
            client-name: kakao
            scope:
              - account_email
        provider:
          kakao:
            authorization-uri: <https://kauth.kakao.com/oauth/authorize>
            token-uri: <https://kauth.kakao.com/oauth/token>
            user-info-uri: <https://kapi.kakao.com/v2/user/me>
            user-name-attribute: id

kakao:
  logout-redirect-uri: 

jwt:
  secret: ${JWT_SECRET}
  access:
    expiration: ${JWT_ACCESS_EXPIRATION}
  refresh:
    expiration: ${JWT_REFRESH_EXPIRATION}

cloud:
  aws:
    credentials:
      access-key: ${AWS_ACCESS_KEY}
      secret-key: ${AWS_SECRET_KEY}
    region:
      static: ap-northeast-2
    s3:
      bucket: ${AWS_S3_BUCKET}

cloudfront:
  domain: ${AWS_CLOUDFRONT_DOMAIN}
  keyPairId: ${CLOUDFRONT_KEY_PAIR_ID}
  privateKeyPath: ${CLOUDFRONT_PRIVATE_KEY_PATH}

application-secret.yml(기존)

SPRING_IMAGE_NAME: spring-project
SPRING_CONTAINER_NAME: backend
SPRING_CONTAINER_PORT: 8081
KAKAO_CLIENT_ID: kakao_client_id
KAKAO_CLIENT_SECRET: kakao_client_secret
DB_URL: jdbc:mysql://<db_url>:3306/<db_name>?serverTimezone=Asia/Seoul&characterEncoding=UTF-8
DB_USERNAME: db_username
DB_PASSWORD: db_password
JWT_SECRET: jwt_secret
JWT_ACCESS_EXPIRATION: 3600000
JWT_REFRESH_EXPIRATION: 604800000
AWS_ACCESS_KEY: aws_access_key
AWS_SECRET_KEY: aws_secret_key
AWS_S3_BUCKET: aws_s3_bucket
AWS_CLOUDFRONT_DOMAIN: aws_cloudfront_domain
CLOUDFRONT_PRIVATE_KEY_PATH: private_key.pem
CLOUDFRONT_KEY_PAIR_ID: cloudfront_key_pair_id

시도 1

  • Jenkins credentials를 통해 application-secret.yml 파일을 서버에 복사한 후 바로 Spring Boot가 읽을 것이라 생각하고 환경변수 참조(${})로 작성함.
  • 파일은 정상적으로 복사되었으나, Spring Boot가 해당 값을 환경변수에서 찾으려 하면서 실패함.

 

시도 2

  • Jenkins 빌드 단계의 Execute Shell에서 export 명령어를 이용해 YAML 파일의 변수를 쉘 환경변수로 등록 시도.
  • export 명령어로는 Jenkins 쉘 내에서만 변수가 유지되고, 실제 Docker 컨테이너 환경에는 전달되지 않아 Spring Boot는 여전히 환경변수를 찾지 못함.

 

시도 3

  • docker-compose.yml 파일의 environment: 블록에 환경변수를 전달하여 컨테이너 내부에 변수를 전달하려 시도.
  • Jenkins 쉘에서 export된 환경변수가 docker-compose 명령어까지 전달되지 않아 실패.
  • 추가로 docker-compose.yml에 환경변수를 다 명시하게 되면, Jenkins Credentials로 파일(application-secret.yml)을 관리할 이유가 없어져 "credentials로 파일 복사"라는 설계 의도 자체가 무너진다는 문제 인식 → 이 방법을 포기함

 

해결책

  • 환경변수(${}) 참조 방식을 사용하지 않고, application-secret.yml 파일을 "완성형" 형태로 변경하여 Spring Boot가 직접 읽고 바로 설정값을 사용할 수 있도록 변경.
  • 즉, application-secret.yml에 데이터베이스 URL과 계정 정보 등 모든 민감한 정보를 하드코딩하여 직접 기입하고, 이를 spring.config.import로 application.yml에서 불러오는 구조로 변경.

application.yml

server:
  servlet:
    context-path: /api/v1
  port: ${SPRING_CONTAINER_PORT}

spring:
  application:
    name: fourcut
  config:
    import: "optional:classpath:application-secret.yml"

application-secret.yml

BUCKET_NAME: bucket_name

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://{mysql_container_name}:3306/{db_name}?serverTimezone=Asia/Seoul&characterEncoding=UTF-8
    username: mysql_username
    password: mysql_password

  jpa:
    database-platform: org.hibernate.dialect.MySQLDialect
    hibernate:
      ddl-auto: update
    properties:
      hibernate:
        show_sql: true
        format_sql: true
        use_sql_comments: true
        dialect: org.hibernate.dialect.MySQLDialect

  security:
    oauth2:
      client:
        registration:
          kakao:
            client-id: kakao_client_id
            client-secret: kakao_client_secret
            redirect-uri: kakao_redirect_uri
            authorization-grant-type: grant_type
            client-authentication-method: kakao_client_authentication_method
            client-name: kakao_client_name
            scope:
              - account_email
        provider:
          kakao:
            authorization-uri: kakao_authorization_uri
            token-uri: kakao_token_uri
            user-info-uri: kakao_user_info_uri
            user-name-attribute: id

kakao:
    logout-redirect-uri: kakao_logout_redirect_uri

jwt:
  secret: jwt_secret
  access:
    expiration: jwt_access_expiration
  refresh:
    expiration: jwt_refresh_expiration

cloud:
  aws:
    credentials:
      access-key: aws_access_key
      secret-key: aws_secret_key
    region:
      static: cloudfront_region_static
    s3:
      bucket: s3_bucket_name

cloudfront:
  domain: aws_cloudfront_domain
  keyPairId: aws_cloudfront_key_pair_id
  privateKeyPath: aws_cloudfront_private_key_path

cookie:
  domain: .film-moa.com
  path: /
  same-site: Strict
  http-only: true
  secure: ${COOKIE_SECURE:true}

 

회고 및 정리

  • Spring Boot는 ${} 표현을 만나면 JVM 환경변수 또는 Docker 컨테이너 환경변수에서 값을 찾기 때문에, ${}를 사용하려면 docker-compose.yml의 environment 설정이나 JVM 시스템 프로퍼티로 반드시 값을 주입해야 한다는 것을 정확히 이해함.
  • application-secret.yml 파일을 Jenkins Credentials로 복사만 했을 경우에는, ${} 변수 치환이 자동으로 일어나지 않는다는 점을 놓쳤고, 이는 JVM이나 Docker 컨테이너 실행 시점에 환경변수로 전달되지 않으면 해결할 수 없다는 것을 알게 됨.
  • docker-compose.yml 파일에 모든 환경변수를 주입하는 방식(environment:)을 쓰려면, 결국 Jenkins Credentials에 파일(application-secret.yml)로 관리하는 의미가 없어지고, 오히려 관리 포인트가 분산되어 복잡해진다는 것을 인식함.
  • 이 때문에 application.yml에서는 최소 설정(server.port, context-path 등)만 남기고, application-secret.yml은 완성된 값(하드코딩 형태)으로 작성하여 Spring Boot가 파일만 읽으면 동작하도록 구조를 바꿈.
  • 결국 민감 정보 관리 방식을 파일 기반으로 통일하고, Jenkins Credentials를 통해 민감 파일을 복사하는 방식으로 간소화하면서, docker-compose.yml은 포트(SPRING_CONTAINER_PORT) 등 필수 최소값만 넘기는 구조로 정리함.
  • 이번 경험을 통해, 배포 파이프라인을 설계할 때 "환경변수 기반 관리"와 "파일 기반 관리"는 서로 전략이 다르며, 둘을 섞으면 복잡성과 오류 가능성이 급격히 올라간다는 것을 체감함. 따라서 초기에 민감정보 관리 방식을 명확히 정의하는 것이 중요함을 배움.
  • Jenkins credentials를 활용할 때, Spring Boot가 참조하는 방식(yml 직접 읽기 vs 환경변수 참조)을 명확히 맞춰야 하며, credentials 파일을 완성형으로 구성하면 설정과 관리가 매우 단순해지고, 트러블슈팅 시간도 대폭 줄일 수 있다는 것을 깨달음.

에러메세지/문제상황

Docker 환경에서 Next.js 애플리케이션을 빌드할 때 다음과 같은 에러가 발생함:

Module not found: Can't resolve '@/app/components/feed/FeedList'
Module not found: Can't resolve '@/components/Menubar'
  • 해당 파일은 실제로 존재함에도 불구하고 Next.js 빌드 중 모듈을 찾을 수 없다는 오류가 발생함
  • 로컬에서는 정상 동작하나, Docker에서만 발생하는 점이 주요 특징이었음

원인

1차 원인

초기 프로젝트 구조가 다음과 같았음:

FRONTEND/
 ├── src/
 │    └── app/
 │         └── components/
 │              └── feed/
 │                   └── FeedList.tsx

components 폴더가 app 폴더 내부에 위치해 있었고, import 경로는 다음과 같았음:

import FeedList from "@/app/components/feed/FeedList";

tsconfig.json은 아래와 같이 설정되어 있었음:

"paths": {
  "@/*": ["./src/*"]
}

따라서 @/app/components/... 경로는 이론상으로 ./src/app/components/...에 대응되므로 올바른 듯 보였으나, 실제로는 Next.js의 App Router 특성과 Webpack path alias 처리 방식 상 라우트 디렉토리인 app/ 내부를 일반 컴포넌트 저장소로 쓰는 것이 적절하지 않음.

⇒ app/components를 쓰면 components가 또 다른 페이지로 간주될 수 있음


2차 원인

구조를 수정한 이후에도 동일한 에러 메시지가 발생했음:

Module not found: Can't resolve '@/components/Menubar'

이후 프로젝트 구조는 다음과 같았음:

FRONTEND/
 ├── src/
 │    ├── app/
 │    └── components/
 │         └── Menubar.tsx

import 경로도 다음과 같이 변경함:

import Menubar from "@/components/Menubar";

하지만 Docker 환경에서 여전히 동일한 에러가 반복됨.

 

시도

시도 1

기존 구조(app/components)에서 components를 src/ 하위로 분리함.

FRONTEND/
 ├── src/
 │    ├── app/
 │    └── components/
 │         └── Menubar.tsx

import 경로도 @/components/... 형태로 수정함.

import FeedList from "@/components/feed/FeedList";

그럼에도 Docker에서 여전히 동일한 Module not found 에러 발생함.


시도 2

  • 경로를 맞췄는데도 같은 에러가 발생하니, import 구문이 잘못쓰였는지, 대소문자 실수가 있었는지, 실제 해당 파일이 존재하는지를 모두 체크함.

 

RUN ls -R /app/src/components/

빌드 로그에서 실제로 FeedList.tsx, Menubar.tsx 등이 /app/src/components 하위에 존재하는 것이 확인됨.


시도 3

"paths": {
  "@/*": ["./src/*"],
  "@/components/*": ["./src/components/*"]
}

그러나 여전히 Docker 빌드에서 동일한 경로 에러 발생함.


시도 4

import path from 'path';

const nextConfig = {
  output: "standalone",
  webpack: (config) => {
    config.resolve.alias = {
      ...config.resolve.alias,
      '@': path.resolve(__dirname, 'src'),
      '@/components': path.resolve(__dirname, 'src/components')
    };
    return config;
  },
};

export default nextConfig;

이후 다시 빌드 시도했으며, 해당 설정 추가 이후부터는 더 이상 경로 관련 에러가 발생하지 않음.

  • nextjs에서 여러 파일들을 하나로 패키징하는 도구
  • 여러 군데에서 작성한 TS/JS 파일들을 하나의 JavaScript 파일로 묶어줌
  • import/export 문법들도 브라우저가 이해할 수 있도록 변환해줌.

⇒’@’ 표시를 src/ 폴더 전체를 의미한다고 지정하고, ‘@/components’ 는 src/components/폴더를 직접 alias하도록 명확히 지정하였음.next.config.ts에 명시적으로 Webpack alias를 추가한 것이 최종 해결책이었음.Webpack 레벨에서 명확하게 경로 alias를 지정해야 Docker/Linux 환경에서도 import 경로 해석이 정확히 이루어졌음.

 

webpack이란?

  • nextjs에서 여러 파일들을 하나로 패키징하는 도구
  • 여러 군데에서 작성한 TS/JS 파일들을 하나의 JavaScript 파일로 묶어줌
  • import/export 문법들도 브라우저가 이해할 수 있도록 변환해줌.

⇒’@’ 표시를 src/ 폴더 전체를 의미한다고 지정하고, ‘@/components’ 는 src/components/폴더를 직접 alias하도록 명확히 지정하였음.

 

해결책

next.config.ts에 명시적으로 Webpack alias를 추가한 것이 최종 해결책이었음.

Next.js에서 App Router와 함께 경로 alias를 사용할 때, 환경에 따라 tsconfig.json만으로는 빌드 단계에서 정확한 경로 해석이 보장되지 않음.

Webpack 레벨에서 명확하게 경로 alias를 지정해야 Docker/Linux 환경에서도 import 경로 해석이 정확히 이루어졌음.

 

회고 및 정리

  • 동일한 코드가 로컬에서는 정상 작동하고 Docker에서만 실패하는 경우, 환경 의존적인 차이(OS, 파일 시스템, 빌드 파서 등)를 먼저 의심해야 함.
  • Next.js는 App Router 아래 구조에 대해 엄격한 처리 방식을 가지고 있으며, 해당 영역에 일반 컴포넌트를 포함하는 것은 바람직하지 않음.
  • 경로 alias는 tsconfig.json과 Webpack 둘 다 일치하게 설정해야 안정적인 빌드가 가능함.
  • 대소문자 구분 문제나 COPY 실패와 같은 낮은 단계 이슈가 아닌, Webpack 빌드 해석기와 alias 충돌이라는 높은 추상화 레벨의 문제였음.
이번 경험을 통해 Docker 환경에서 Next.js를 사용하는 경우, import 경로 alias는 반드시 Webpack과 tsconfig에 이중 정의하고, App Router 디렉토리와 컴포넌트 디렉토리는 분리하는 것이 안정적임을 배웠다!

에러메세지 / 문제상황

  • docker ps 명령어 결과에서 spring-container가 8080-8081/tcp 식으로 포트 범위를 열고 있는 것이 확인됨.
  • nginx를 통한 API 요청 시 502 Bad Gateway 오류 발생.

원인

  • Dockerfile과 docker-compose.yml의 포트 설정 불일치.
  • Dockerfile에서는 EXPOSE 8081로 되어 있었고,
  • docker-compose.yml에서는 expose: 8080, environment: SPRING_CONTAINER_PORT=8080으로 설정되어 있었음.
  • Spring Boot 서버는 8080 포트에서 동작했지만, Docker 메타데이터(EXPOSE)에는 8081이 등록되어 있었기 때문에 docker ps 출력에서 8080-8081 포트 범위가 표시되었음.
  • nginx는 spring-container의 8080 포트를 proxy_pass 해야 했는데, 포트 정보가 꼬여 502 오류 발생.

시도

시도 1: docker-compose.yml 포트 설정 정비

  • docker-compose.yml에서 expose: 8080으로 수정하여 컨테이너 내부 포트를 8080으로 설정함.
  • Spring Boot 서버가 8080 포트에서 실행되도록 environment: SPRING_CONTAINER_PORT=8080 설정을 추가함.
  • 그러나 docker ps 결과에서 여전히 8080-8081/tcp 식으로 두 포트가 동시에 열려 있는 것으로 표시되는 문제가 발생함.

시도 2: 이미지 캐시 문제를 고려한 강제 리빌드

  • Dockerfile 변경사항이 적용되지 않은 캐시 이미지 문제를 의심하여 -no-cache 옵션으로 이미지를 강제 리빌드함.
  • Jenkins Execute Shell 스크립트에서 다음 명령어를 사용함.
docker compose build --no-cache spring-container
  • 강제 리빌드 후에도 docker ps 출력에서 포트 문제는 여전히 해결되지 않음.

시도 3: docker image inspect를 통한 이미지 메타데이터 확인

  • 문제의 정확한 원인을 찾기 위해 docker image inspect spring-project:latest 명령어를 통해 이미지를 분석함.
  • 분석 결과, 이미지의 ExposedPorts 항목에 8081/tcp가 남아 있는 것을 확인함.
"ExposedPorts": {
  "8081/tcp": {}
}
  • 이를 통해 문제는 docker-compose.yml 설정이나 캐시 문제가 아니라 Dockerfile 자체의 EXPOSE 설정 오류임을 파악함.

시도 4: Dockerfile 수정 및 최종 배포

  • Dockerfile의 EXPOSE 8081을 EXPOSE 8080으로 수정함.
  • docker-compose.yml의 expose: 8080 설정은 그대로 유지함.
  • nginx 설정 파일의 proxy_pass 대상 포트도 8080으로 통일함.
  • 이후 수정된 Dockerfile을 기반으로 docker compose build --no-cache를 수행하여 이미지를 강제 리빌드함.
  • 컨테이너를 재배포 (docker compose up -d)하고, nginx도 reload하여 변경사항을 적용함.
  • 최종적으로 docker ps 결과는 8080/tcp로 정상화되었고, API 통신 시 발생했던 502 오류도 모두 해결됨.

 

해결책

  • Dockerfile의 EXPOSE 포트를 실제 서버가 구동하는 포트(8080)와 일치시킴.
  • docker-compose.yml에서도 expose: 8080으로 설정하여 내부 네트워크 통신용 포트를 정확히 열어줌.
  • nginx 설정 파일에서도 proxy_pass 대상을 spring-container:8080으로 정확히 매칭함.
  •  Spring Boot 애플리케이션이 실제로 8080 포트에서 구동되도록 하기 위해, application.yml에 server.port=${SPRING_CONTAINER_PORT}를 설정함.
  • 이는 docker-compose.yml 파일의 environment: 부분(SPRING_CONTAINER_PORT=8080)과 연결되어, 컨테이너 구동 시 Spring 서버 포트를 제어할 수 있도록 구성함.
  • 결과적으로 docker-compose와 nginx는 Spring 서버의 실제 구동 포트를 기준으로 포트 매칭만 담당하는 역할을 하게 됨.

 

회고 및 정리

  • Dockerfile의 EXPOSE는 네트워크 자체를 여는 설정은 아니지만, docker inspect와 docker ps 결과에는 표시되기 때문에 주의 필요
  • docker-compose.yml의 expose는 Docker 네트워크 내부 통신에만 영향을 주고, ports는 외부(호스트) 노출에만 영향을 준다.
  • 실제 포트가 열리려면 Spring Boot 서버의 server.port 설정이 정확해야 하며, 이 설정이 최우선.
  • Dockerfile과 docker-compose.yml의 포트 설정이 일치하지 않으면 운영 및 디버깅 과정에서 혼란을 초래할 수 있다
  • 향후에는 Dockerfile, docker-compose.yml, nginx 설정의 포트 번호를 반드시 함께 통일해서 관리해야 한다.

에러메세지 / 문제상황

MySQL Docker 컨테이너 실행 직후 자동 종료됨. docker logs 결과:

mysqld: Cannot change permissions of the file 'ca.pem' (OS errno 1 - Operation not permitted)
[ERROR] Could not set file permission for ca.pem
[ERROR] The designated data directory /var/lib/mysql/ is unusable.

 

 

원인

MySQL은 실행될 때 데이터를 저장할 공간(폴더)이 필요함.

그 저장 공간을 Windows 컴퓨터 안의 폴더(예: C드라이브 경로)로 연결했고, MySQL은 해당 폴더 안에서 파일 권한을 바꾸거나 소유자를 설정하려고 함.

그러나 Windows 폴더는 이런 리눅스식 작업을 허용하지 않기 때문에, MySQL이 필요한 작업을 하지 못해 실행이 중단됨.

→ 따라서 Windows 폴더는 리눅스 프로그램이 자유롭게 다룰 수 없는 구조라서 문제가 발생함.

시도 1

  • Windows 측의 마운트 디렉토리 삭제 후 재시도
  • 동일한 오류 발생

시도 2

  • data 생성 디렉토리를 바꿔 컨테이너 실행했으나 같은 오류 발생
  • mysql_data → mysql_data2

해결책

Windows 쪽 폴더를 쓰는 게 아니라, Ubuntu(리눅스) 안에 있는 폴더를 사용하도록 수정함:

mkdir -p ~/docker/docker-mysql/mysql_data

docker run -e MYSQL_ROOT_PASSWORD=**** \\
  -d -p 3306:3306 \\
  -v ~/docker/docker-mysql/mysql_data:/var/lib/mysql \\
  mysql

→ 리눅스 내부 폴더는 MySQL이 자유롭게 접근하고 설정을 바꿀 수 있기 때문에, 컨테이너가 정상적으로 실행됨

회고 및 정리

  • Docker에서 데이터를 저장할 때는 'Ubuntu 안의 폴더'를 사용하는 것이 안전하다
  • Windows 폴더를 연결하면 권한 문제로 오류가 생길 수 있음
  • 특히 MySQL처럼 데이터를 직접 다루는 프로그램은 리눅스 폴더를 쓰는 게 필수

→ 앞으로는 /mnt/c/... 같은 경로 대신 ~/폴더명처럼 리눅스 안에 있는 경로를 쓰는 게 좋다!

 

에러메세지 / 문제상황

Docker 컨테이너(MySQL)를 실행하려 하자 다음과 같은 에러 발생:

Error response from daemon: Ports are not available: exposing port TCP 0.0.0.0:3306 → 127.0.0.1:0: listen tcp 0.0.0.0:3306: bind: Only one usage of each socket address is normally permitted.

→ docker start 또는 docker run 실행 시 3306 포트 충돌로 인해 컨테이너 시작 실패

 

 

원인

Windows에서

이미 다른 프로세스가 3306 포트를 선점 중

이어서 Docker가 동일 포트에 바인딩하지 못함.

(Docker는 기본적으로 MySQL 컨테이너에 3306 포트를 사용)

시도 1

WSL(Ubuntu) 환경에서 포트 사용 확인:

lsof -i:3306

 

 

❌ 결과 없음 → WSL 내부에서는 포트 점유 프로세스가 보이지 않음

 

시도 2

Windows PowerShell에서 포트 점유 확인:

netstat -aon | findstr :3306

→ PID 6700이 3306 포트를 LISTENING 상태로 사용 중인 것을 확인

tasklist | findstr 6700

→ 실제 어떤 프로세스인지 확인 후 강제 종료:

taskkill /PID 6700 /F

 

해결책

3306 포트를 점유하고 있는 백그라운드 MySQL 프로세스 또는 기타 앱을 종료하여 포트를 비워줌

docker run

명령이 정상적으로 실행됨

 

회고 및 정리

  • WSL에서 보이지 않는 포트 충돌은 Windows 쪽 프로세스일 가능성이 높다
  • 이럴 땐 netstat과 tasklist를 활용해 포트를 선점한 프로세스를 찾아내는 것이 핵심
  • Docker 포트 바인딩 에러는 대부분 "다른 앱이 해당 포트를 쓰고 있음" → 포트를 바꾸거나 기존 앱을 중지하면 해결 가능