Platform
Docker 기반 로컬 개발환경과 데이터베이스 컨테이너 운영
Docker 이미지, 컨테이너, 포트, 볼륨, MySQL/PostgreSQL 컨테이너를 로컬 백엔드 개발환경 관점에서 정리합니다.
Docker를 처음 배울 때는 docker run hello-world처럼 명령어 중심으로 시작하게 된다. 하지만 백엔드 개발환경에서 Docker가 진짜 유용해지는 지점은 조금 다르다. 애플리케이션 서버, PostgreSQL, MySQL, Redis 같은 의존 서비스를 팀원이 같은 방식으로 띄우고, 같은 포트와 같은 초기 데이터로 테스트할 수 있을 때 Docker는 단순 실행 도구가 아니라 개발환경의 기준점이 된다.
이 글은 Docker 기본 명령어, 이미지와 컨테이너, Docker Hub, MySQL/PostgreSQL 컨테이너 실습을 로컬 백엔드 개발환경 운영 흐름으로 다시 정리한 기록이다. 목표는 명령어를 많이 외우는 것이 아니라, 컨테이너가 재시작되어도 데이터가 유지되는지, 로그는 어디서 보는지, 포트 충돌은 어떻게 피하는지, 운영 환경으로 넘어갈 때 어떤 가정이 깨지는지 확인하는 것이다.
이미지와 컨테이너를 분리해서 이해하기
이미지는 실행 환경의 템플릿이고, 컨테이너는 이미지를 실제 프로세스로 실행한 단위다. 이미지는 애플리케이션 코드, 런타임, 라이브러리, 기본 명령어, 환경 변수 기본값을 담을 수 있다. 컨테이너는 그 이미지를 기준으로 만들어진 실행 중인 프로세스다.
이미지 이름은 보통 다음 구조를 가진다.
[Host[:Port]/[Namespace/]Repository[:Tag]]
ubuntu:24.04
postgres:16
ghcr.io/example/api:1.0.0
myregistry.local:5000/platform/api:2025.09.01태그를 생략하면 보통 latest가 붙지만, 배포와 재현성을 생각하면 latest에 의존하지 않는 편이 좋다. 같은 latest라도 시간이 지나면 실제 이미지 내용이 달라질 수 있다. 로컬 실습에서는 편하지만, CI/CD나 Kubernetes 배포에서는 명확한 버전 태그나 digest를 사용하는 편이 추적하기 쉽다.
docker image pull ubuntu:20.04
docker image pull ubuntu:22.04
docker image pull ubuntu:24.04
docker image ls ubuntu
docker image pull ruby:3.2.2
docker image inspect ruby:3.2.2
docker image history ruby:3.2.2컨테이너는 내부에서 실행되는 PID 1 프로세스와 운명을 같이한다. 컨테이너가 “서버”처럼 보이더라도 본질적으로는 하나의 주 프로세스를 격리해서 실행하는 방식이다. PID 1 프로세스가 끝나면 컨테이너도 종료된다.
docker run --interactive --tty ubuntu bash
hostname
whoami
head -n 4 /etc/os-release
echo $SHELL
exit
docker container ls
docker container ls --all컨테이너 상태에는 created, running, paused, restarting, exited, dead, removing이 있다. 백엔드 서비스를 다룰 때는 exited만 봐도 충분하지 않다. 왜 종료되었는지 docker logs, exit code, 애플리케이션 초기화 로그를 함께 봐야 한다.
컨테이너는 VM과도 다르다. VM은 하이퍼바이저 위에서 게스트 OS를 실행하지만, 컨테이너는 호스트 커널을 공유하면서 프로세스를 격리한다. 그래서 시작이 빠르고 이미지가 작지만, 커널을 공유한다는 특성 때문에 보안 경계와 권한 설정을 신경 써야 한다.
Virtual Machine
- hypervisor
- guest OS per VM
- strong isolation
- heavier startup and image size
Container
- shared host kernel
- isolated process namespace
- faster startup
- needs careful image and runtime security운영 이미지에서는 root 사용자로 실행하지 않는 것이 좋다. 모든 이미지가 바로 non-root로 동작하는 것은 아니지만, 애플리케이션 이미지라면 실행 계정을 명확히 두는 편이 안전하다.
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
RUN addgroup -S app && adduser -S app -G app
USER app
CMD ["node", "server.js"]이런 설정은 Kubernetes의 securityContext와도 연결된다. 로컬 Docker에서 root 권한을 당연하게 쓰면 클러스터에서도 같은 습관이 이어질 수 있다.
실행, 접속, 삭제 흐름을 익히기
가장 기본적인 실행은 다음과 같다.
docker container run hello-world
docker container run --name hello hello-world
docker container ls --all
docker container rm hello대화형 컨테이너는 -it 옵션을 사용한다.
docker container run --rm --interactive --tty python python3
>>> print("Hello Docker")
>>> exit()이미 실행 중인 컨테이너 안으로 들어가거나 명령을 실행할 때는 exec를 쓴다.
docker container exec psdb head -n 4 /etc/os-release
docker container exec -it psdb bash컨테이너를 정리할 때는 실행 중인지, 정지 상태인지 확인해야 한다.
docker container stop <container_id_or_name>
docker container rm <container_id_or_name>
docker container rm --force <container_id_or_name>
docker container prune--rm은 컨테이너가 종료될 때 자동으로 삭제한다. 일회성 테스트에는 편하지만, 종료 후 상태를 살펴봐야 하는 경우에는 로그나 컨테이너 상태를 잃을 수 있다. 데이터베이스처럼 상태를 가진 컨테이너에는 더 조심해야 한다.
포트 매핑은 로컬 개발과 운영 노출을 구분해야 한다
컨테이너 내부의 포트와 호스트에서 접근하는 포트는 다르다. NGINX 컨테이너는 내부에서 80번 포트를 쓰지만, 로컬에서는 8080으로 매핑할 수 있다.
docker container run --rm --publish 8080:80 nginx
curl http://localhost:8080이 매핑은 호스트포트:컨테이너포트 순서다. 로컬에서 여러 서비스를 띄우면 포트 충돌이 자주 난다. 예를 들어 로컬에 PostgreSQL이 이미 5432를 쓰고 있다면 컨테이너의 5432를 호스트 15432에 매핑할 수 있다.
docker container run \
--name psdb \
--detach \
--env POSTGRES_PASSWORD=toor \
--publish 15432:5432 \
postgres:16운영 환경에서는 포트를 외부에 직접 공개하는 방식이 달라진다. Kubernetes에서는 Service와 Ingress가 앞에 서고, 클라우드에서는 Load Balancer나 API Gateway가 진입점이 된다. 로컬의 -p 8080:80은 접근 확인을 위한 편의 장치이지, 운영 노출 설계 자체는 아니다.
컨테이너 네트워크는 이름 기반 통신부터 이해한다
로컬에서 컨테이너 하나만 실행할 때는 localhost와 포트 매핑만 봐도 충분해 보인다. 하지만 API, DB, Redis가 함께 뜨면 컨테이너 간 통신을 이해해야 한다. Docker bridge network에서는 같은 네트워크에 붙은 컨테이너가 컨테이너 이름으로 서로를 찾을 수 있다.
docker network create app-net
docker run --name postgres \
--network app-net \
-e POSTGRES_PASSWORD=app \
-d postgres:16
docker run --name redis \
--network app-net \
-d redis:7이 네트워크 안에서 API 컨테이너는 postgres:5432, redis:6379로 접근할 수 있다. 호스트에서 접근할 때는 포트 매핑이 필요하지만, 컨테이너끼리는 같은 bridge network 안에서 이름 기반으로 통신한다.
Host laptop
-> localhost:15432
-> postgres container:5432
API container
-> postgres:5432
-> redis:6379이 차이를 모르고 컨테이너 안에서 localhost:5432로 DB에 붙으려 하면 실패한다. 컨테이너 안의 localhost는 호스트가 아니라 자기 자신이다. 이 감각은 Kubernetes에서도 그대로 이어진다. Pod 내부 localhost, Service DNS, Ingress host가 각각 다른 경계를 가진다.
이미지를 직접 만들 때 확인할 것
정적 HTML을 NGINX 이미지에 넣는 가장 단순한 Dockerfile은 다음처럼 작성할 수 있다.
FROM nginx:1.27
COPY . /usr/share/nginx/htmldocker build -t nginx:test .
docker run --rm -p 8080:80 nginx:testNode.js 애플리케이션은 보통 작업 디렉터리와 의존성 설치, 실행 명령을 정의한다.
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
EXPOSE 3000
CMD ["npm", "start"]여기서 자주 나는 실수는 node_modules, 빌드 산출물, 로컬 설정 파일을 불필요하게 이미지에 넣는 것이다. .dockerignore를 함께 관리해야 이미지가 작아지고 빌드 캐시가 안정된다.
node_modules
.next
dist
.env
.git이미지 빌드 후에는 히스토리와 크기를 확인한다.
docker image ls
docker image history nginx:test
docker image inspect nginx:test운영에 가까운 이미지는 다음 질문에 답할 수 있어야 한다.
- 태그로 어떤 소스 버전에서 빌드됐는지 추적 가능한가
- 런타임에 필요 없는 빌드 도구가 이미지에 남아 있지 않은가
- Secret이나
.env파일이 이미지 레이어에 들어가지 않았는가 - 로그가 파일에만 쌓이지 않고 stdout/stderr로 나오는가
- 컨테이너 종료 신호를 애플리케이션이 정상 처리하는가
Docker Hub와 Registry는 배포 경로의 일부다
Docker Hub나 GitHub Container Registry, AWS ECR, Azure ACR은 이미지를 저장하고 배포하는 Registry다. 로컬에서 만든 이미지를 팀이나 배포 파이프라인이 사용하려면 태그를 붙이고 push해야 한다.
docker login
docker tag myapp:1.0 username/myapp:1.0
docker push username/myapp:1.0
docker pull username/myapp:1.0실습에서는 httpd 컨테이너에 접속해 index.html을 바꾼 뒤 docker commit으로 이미지를 만들 수 있다.
docker pull httpd
docker run --name apache -p 8080:80 httpd
docker exec -it apache /bin/bash
docker commit apache username/apache-demo
docker push username/apache-demo이 방식은 컨테이너와 이미지 관계를 이해하기에는 좋지만, 실제 협업에서는 Dockerfile 기반 빌드가 더 낫다. docker commit은 어떤 변경이 들어갔는지 코드 리뷰가 어렵고, 같은 이미지를 재현하기 힘들다. 이미지는 가능한 한 Dockerfile과 CI 로그로 재현 가능해야 한다.
데이터베이스 컨테이너는 볼륨 없이는 일회성이다
MySQL이나 PostgreSQL을 컨테이너로 띄우면 로컬 개발이 편해진다. 하지만 DB는 상태를 가진 시스템이다. 컨테이너를 지우면 내부 파일시스템도 사라지므로, 데이터 유지가 필요하면 볼륨을 사용해야 한다.
MySQL은 환경 변수로 초기 root password, 사용자, DB를 만들 수 있다.
docker container run \
--name mysql-db \
--detach \
--env MYSQL_ROOT_PASSWORD=toor \
--env MYSQL_USER=tony \
--env MYSQL_PASSWORD=pass1234 \
--env MYSQL_DATABASE=mydb00 \
--publish 3306:3306 \
--volume mysql-data:/var/lib/mysql \
mysql:8접속 후 간단한 테이블을 만들 수 있다.
mysql --host=127.0.0.1 --port=3306 --user=tony --password=pass1234 mydb00show databases;
select database();
create table t1(id int, name varchar(10));
insert into t1 values (10, 'Jane');
insert into t1 values (20, 'Alice');
insert into t1 values (30, 'Tom');
select * from t1;PostgreSQL도 비슷하게 실행할 수 있다.
docker container run \
--name psdb \
--detach \
--env POSTGRES_PASSWORD=toor \
--publish 5432:5432 \
--volume postgres-data:/var/lib/postgresql/data \
postgres:16psql --host=127.0.0.1 --port=5432 --user=postgres초기 실행이 실패했다면 로그부터 확인한다.
docker container logs psdb
docker container logs --follow psdbDB 컨테이너를 운영하듯 다뤄보면 볼륨, 초기화 스크립트, 계정 권한, 포트, 백업이 자연스럽게 보인다. 로컬 개발에서는 편의를 위해 비밀번호를 단순하게 둘 수 있지만, 실제 환경에서는 Secret 관리와 네트워크 접근 제한을 함께 고려해야 한다.
볼륨은 컨테이너 수명과 데이터를 분리한다. 하지만 볼륨이 있다고 해서 백업이 된 것은 아니다. 볼륨은 같은 호스트 안에서 컨테이너를 지워도 데이터를 남기는 장치이고, 백업은 장애나 실수 후 복구할 수 있는 별도 사본을 만드는 일이다.
docker volume ls
docker volume inspect postgres-data로컬 PostgreSQL 볼륨을 덤프하려면 컨테이너 안의 도구를 사용할 수 있다.
docker exec psdb pg_dump -U postgres postgres > backup.sql복구도 별도로 확인해야 한다.
cat backup.sql | docker exec -i psdb psql -U postgres postgresPowerShell에서는 redirection 처리 방식이 다를 수 있으므로, 실제 운영 자동화에서는 CI runner나 Linux shell 기준으로 검증된 스크립트를 두는 편이 낫다. 중요한 것은 “볼륨이 있으니 안전하다”가 아니라, 백업 파일을 만들고 다른 환경에서 복구해본 적이 있는지다.
Compose로 의존 서비스를 함께 띄우기
실제 백엔드 개발환경에서는 API 서버 하나만 뜨지 않는다. PostgreSQL, Redis, 메시지 브로커, 검색 엔진, 관측성 도구가 함께 필요할 수 있다. 이때 docker run 명령을 길게 반복하기보다 Docker Compose로 의존 서비스를 선언하는 편이 관리하기 쉽다.
services:
api:
build:
context: .
ports:
- "3000:3000"
environment:
DATABASE_URL: postgresql://app:app@postgres:5432/app
REDIS_URL: redis://redis:6379
depends_on:
- postgres
- redis
postgres:
image: postgres:16
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: app
POSTGRES_DB: app
ports:
- "5432:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
redis:
image: redis:7
ports:
- "6379:6379"
volumes:
postgres-data:docker compose up -d
docker compose ps
docker compose logs -f api
docker compose downdepends_on은 컨테이너 시작 순서를 어느 정도 맞춰주지만, 데이터베이스가 실제로 쿼리를 받을 준비가 됐는지는 보장하지 않는다. 애플리케이션은 DB 연결 재시도나 readiness check를 가져야 한다. Kubernetes로 넘어가도 같은 문제가 반복된다. Pod가 떴다는 사실과 서비스가 준비됐다는 사실은 다르다.
로컬 Docker 환경에서 확인할 운영 가정
Docker 실습을 하면서 가장 크게 남은 기준은 “컨테이너가 뜬다”보다 “재현 가능한 실행 단위인가”였다. 다음 항목을 체크하면 단순 실습을 넘어 실제 개발환경에 가까워진다.
Runtime
- 컨테이너가 foreground 프로세스로 실행되는가
- SIGTERM을 받았을 때 정상 종료되는가
- 로그가 stdout/stderr로 나오는가
Configuration
- 환경별 설정이 이미지에 고정되어 있지 않은가
- Secret이 Dockerfile이나 이미지 레이어에 들어가지 않았는가
- 기본 포트와 로컬 매핑 포트가 문서화되어 있는가
Data
- DB 데이터가 볼륨에 저장되는가
- 초기 스키마나 seed 데이터가 재현 가능한가
- 컨테이너 삭제와 볼륨 삭제의 차이를 알고 있는가
Build
- Dockerfile로 이미지가 재현 가능한가
- .dockerignore가 정리되어 있는가
- 이미지 태그가 배포 이력과 연결되는가이 기준은 Kubernetes에서도 그대로 이어진다. Docker 이미지가 불안정하면 Deployment도 불안정하다. 로컬에서 환경 변수가 뒤섞이면 Helm values도 뒤섞인다. 로그가 표준 출력으로 나오지 않으면 클러스터 로그 수집도 어려워진다. 그래서 Docker는 Kubernetes 이전 단계의 가벼운 실습이 아니라, 운영 가능한 애플리케이션 단위를 만드는 출발점으로 보는 편이 맞다.
정리
Docker는 컨테이너를 실행하는 도구이지만, 백엔드 개발에서는 로컬 환경을 일관되게 재현하는 기준점이 된다. 이미지와 컨테이너를 구분하고, PID 1과 로그, 포트 매핑, 볼륨, Registry, Dockerfile, Compose를 함께 이해하면 “내 컴퓨터에서만 된다”는 문제를 꽤 줄일 수 있다.
특히 PostgreSQL, MySQL, Redis 같은 의존 서비스를 컨테이너로 다뤄보면 상태를 가진 시스템을 어떻게 격리하고 유지할지 감이 생긴다. 컨테이너를 지우면 무엇이 사라지고, 볼륨을 남기면 무엇이 유지되며, 환경 변수를 바꾸면 어떤 초기화가 다시 일어나지 않는지 직접 확인하게 된다.
이 흐름은 그대로 Kubernetes로 이어진다. Docker에서 이미지, 포트, 환경 변수, 볼륨, 로그를 명확히 다루지 못하면 Kubernetes의 Deployment, Service, ConfigMap, Secret, PVC도 흐릿해진다. 반대로 로컬 Docker 환경을 재현 가능하게 만들어두면 CI와 클러스터 배포로 넘어갈 때 훨씬 덜 흔들린다.
이 글의 무단 복제, 재배포, 자동 수집을 허용하지 않습니다. 인용이 필요한 경우 출처와 원문 링크를 함께 남겨주세요.