리눅스 환경에서 서비스를 운영하다 보면 환경에 따라 선호되는 소프트웨어 스택이 달라진다.
예를 들어, 일반적인 리눅스(Bare Metal) 환경에서는 웹 서버로 Apache2를, FTP 서버로는 vsFTP를, 데이터베이스로는 PostgreSQL이나 MariaDB를 주로 사용한다.
하지만 컨테이너 기반인 Docker 환경에서는 조금 다르다.
웹 서버는 가벼운 Nginx가, FTP 서버는 설정이 간편하고 가벼운 Pure-FTPd나 wuFTP가, 데이터베이스는 MySQL이 주로 사용된다.
이번에는 Docker 환경에 최적화된 Pure-FTPd를 사용하여 FTP 서버를 구축하는 과정을 알아보자.
특히 FTP의 동작 원리인 Passive Mode를 이해하고 이를 Docker 네트워크 설정에 반영하는 과정이 중요하다.
1. FTP 프로토콜의 이해: 왜 포트가 두 개 필요한가?
FTP(File Transfer Protocol) 서버를 구축하려면 먼저 FTP가 통신하는 방식을 이해해야 한다.
HTTP와 달리 FTP는 두 개의 연결(Connection)을 동시에 사용한다는 점이 특징이다.
1.1 제어 채널과 데이터 채널
- 제어 채널 (Control Connection, 포트 21): 클라이언트가 서버에 접속하여 로그인하고, 명령(파일 목록 요청, 업로드 요청 등)을 보내는 통로다.
- 데이터 채널 (Data Connection, 임의의 포트): 실제 파일의 내용이나 폴더 목록 데이터를 주고받는 통로다.
1.2 Active 모드 vs Passive 모드
데이터 채널을 누가 연결하느냐에 따라 두 가지 모드로 나뉜다.
- Active 모드: 클라이언트가 포트를 열고 기다리면, 서버가 클라이언트에게 접속한다. (클라이언트 쪽에 방화벽이 있으면 접속이 차단되는 문제가 있다.)
- Passive 모드: 서버가 임의의 포트(예: 30000~30009)를 열고 기다리면, 클라이언트가 서버에게 접속한다.
요즘 대부분의 클라이언트는 방화벽 뒤에 있으므로 Passive 모드가 표준처럼 사용된다.
특히 Docker는 NAT(Network Address Translation) 환경과 유사하므로 반드시 Passive 모드를 설정하고 데이터 채널용 포트 범위를 미리 개방해 두어야 한다.
아래는 Passive 모드에서의 접속 흐름을 나타낸 시퀀스 다이어그램이다.

2. 방법 1️⃣: 컨테이너 내부로 들어가서 사용자 설정
첫 번째 방식은 기본 이미지를 실행한 뒤, 컨테이너 내부에 접속하여 리눅스 명령어로 직접 사용자를 생성하는 방법이다.
Pure-FTPd의 내부 인증 구조를 이해하는 데 도움이 된다.
2.1 컨테이너 실행
먼저 gimoh/pureftpd 이미지를 사용하여 컨테이너를 실행한다.
앞서 설명한 Passive 모드를 위해 데이터 포트 범위를 반드시 -p 옵션으로 열어줘야 한다.
docker run -d --name=ftpd1 \
-p 21:21 \
-p 30000-30009:30000-30009 \
gimoh/pureftpd \
-c 3 -j -l puredb:/etc/pureftpd.pdb \
-p 30000:30009 -P localhost
- -p 21:21: 제어 채널 포트 매핑.
- -p 30000-30009:30000-30009: 데이터 채널(Passive Mode)을 위한 포트 범위를 호스트와 컨테이너를 1:1로 매핑한다. 즉, "내 컴퓨터의 로컬호스트"로 들어온 신호를 "컨테이너의 로컬호스트"로 토스하는 것.
- -l puredb:/etc/pureftpd.pdb: Pure-FTPd 자체 DB 파일을 이용해 사용자를 인증한다.
- -P localhost: 클라이언트에게 Passive 모드 응답을 보낼 때, 자신의 IP를 localhost라고 알려준다. (실제 서버라면 공인 IP를 적어야 한다.)

2.2 내부 접속 및 사용자 생성
컨테이너 실행 후, 내부 쉘에 접속하여 시스템 계정과 FTP 전용 가상 유저를 생성한다.
1) 컨테이너 내부 쉘 접속
docker exec -it ftpd1 sh
2) 기반이 될 시스템 계정 생성 (비밀번호 불필요)
/ # adduser -D ftpuser
3) Pure-FTPd 가상 유저(snowman) 생성
/ # pure-pw useradd snowman -u ftpuser -d /home/paul
Password: (비밀번호 입력)
Enter it again: (비밀번호 확인)
- -u ftpuser: snowman이 업로드한 파일의 실제 소유자는 리눅스의 ftpuser가 된다.
- -d /home/ snowman : snowman의 홈 디렉토리 지정
4) DB 갱신 (이 명령어를 입력해야 설정이 적용됨)
/ # pure-pw mkdb
5) 접속 종료
/ # exit
2.3 접속 테스트
로컬에서 FTP 클라이언트로 접속하여 파일 업로드를 테스트한다.
# 로컬에서 FTP 접속
ftp -p localhost 21
Name (localhost:ubuntu): snowman
Password:
ftp> !echo "Hello Docker FTP" > test.txt # 로컬에 테스트 파일 생성
ftp> put test.txt # 파일 업로드
227 Entering Passive Mode (127,0,0,1,117,51) # Passive 모드 동작 확인
226-File successfully transferred
3. 방법2️⃣: 환경 변수를 이용한 자동화
두 번째 방식은 stilliard/pure-ftpd 이미지를 사용하여 컨테이너 실행 시점에 환경 변수(-e)로 사용자 설정을 주입하는 방법이다.
이 방식은 Infrastructure as Code (IaC) 철학에 부합하며, 컨테이너를 다시 띄워도 설정이 유지되도록 구성하기 쉽다.
3.1 컨테이너 실행 (환경 변수 + 볼륨 마운트)
이번에는 포트를 2001번으로 설정하고, 호스트의 디렉토리를 볼륨으로 연결하여 데이터 영속성을 확보한다.
docker run -d --name ftpd2 \
-p 2001:21 \
-p 40000-40009:40000-40009 \
-v $PWD:/home/ftpusers/ \
-e PUBLICHOST=localhost \
-e FTP_USER_NAME=snowman \
-e FTP_USER_PASS=1234 \
-e FTP_USER_HOME=/home/snowman \
-e FTP_PASSIVE_PORTS=40000:40009 \
stilliard/pure-ftpd:latest
- -p 2001:21 : 호스트의 2001번 포트를 컨테이너의 21번(제어 포트)으로 연결. 보안상 기본 포트를 피할 때 유용하다.
- -p 40000-40009:40000-40009 : ftpd1과 다른 Passive 포트 대역을 할당했다.
- -v $PWD:/home/ftpusers/ : 호스트의 현재 경로($PWD)를 컨테이너의 데이터 경로에 마운트한다. 이렇게 하면 컨테이너를 삭제해도 업로드된 파일은 호스트에 안전하게 남는다.
- FTP_USER_NAME 등의 환경 변수 : 컨테이너 시작 시 자동으로 사용자를 생성해준다. docker exec으로 컨테이너 내부로 들어가서 설정할 필요가 없다.
3.2 접속 테스트
포트가 2001번으로 변경되었으므로 접속 시 포트를 명시해야 한다.
ftp -p localhost 2001
Connected to localhost.
...
ftp> !echo "Success Test" > success.txt
ftp> put success.txt
227 Entering Passive Mode (127,0,0,1,156,72)
226-File successfully transferred
업로드가 완료되면 호스트의 현재 디렉토리(ls -l)에서 success.txt 파일이 생성된 것을 확인할 수 있다.
4. 정리
FTP는 오래된 프로토콜이지만, 대용량 파일 전송이나 레거시 시스템 연동 등 여전히 현업에서 자주 사용된다.
Docker의 네트워크 구조와 FTP의 동작 방식을 정확히 이해한다면, 가볍고 효율적인 파일 서버를 쉽게 구축할 수 있을 것이다.
'AI Journey > 클라우드' 카테고리의 다른 글
| [Docker Swarm] 서비스와 레플리카, 스케일링 및 글로벌 모드 이해하기 (0) | 2026.01.14 |
|---|---|
| [Docker Swarm] 도커 스웜으로 만드는 컨테이너 클러스터 (0) | 2026.01.14 |
| [Docker] Nginx 로그를 Fluentd를 통해 MongoDB에 저장하기 (중앙 집중형 로그 관리) (0) | 2026.01.12 |
| [k8s] NestJS 앱을 도커라이징해서 Pod로 띄우기 (0) | 2026.01.11 |
| [Docker] 리소스 모니터링 도구 - events, stats, system, cAdvisor (0) | 2026.01.08 |