Docker 와 서버 관리
도커는 컨테이너 기반 오픈소스 가상화 플랫폼이다.
컨테이너(OS 내 격리된 환경에서 동작하는 프로세스) 를 제공하지만 VM 처럼 물리적 하드웨어를 Hypervisor 로 가상화하지 않고 OS 를 Docker 로 가상화한다.

도커는 별도 Guest OS 를 설치할 필요 없이 Host OS 의 커널을 공유한다.
Guest OS 에서 필요한 커널 및 시스템 파일에 대한 메모리가 절약되고 공유된 커널로 즉시 실행이 가능해지는 장점이 있다.
도커 컨테이너는 서버를 배포하는 방식의 표준을 이미지로 제공한다.
이미지는 컨테이너 실행에 필요한 설정을 담은 패키지로 도커가 설치되어있는 환경이라면 어디서든 (이식성) 컨테이너로 생성되고 실행될 수 있다. (확장성)
실습 1: PostgreSQL 컨테이너 생성과 볼륨
실습 예제로 DB 컨테이너를 생성하고 데이터를 관리하는 예제를 다뤄보려고 한다.
먼저 Docker for Mac 을 설치해주고 docker version 명령어를 실행해보면 다음과 같은 결과를 볼 수 있다.
Client:
Cloud integration: v1.0.22
Version: 20.10.x
API version: 1.41
Go version: go1.16.12
Git commit: e91ed57
Built: Mon Dec 13 11:46:56 2021
OS/Arch: arm64 (Mac)
Context: default
Experimental: true
Server: Docker Desktop 4.5.0 (74594)
Engine:
Version: 20.10.x
API version: 1.41 (minimum version 1.12)
Go version: go1.16.12
Git commit: 459d0df
Built: Mon Dec 13 11:43:07 2021
OS/Arch: linux/arm64
Experimental: false
containerd:
Version: 1.4.12
GitCommit: 7b11cfaabd73bb80907dd23182b9347b4245eb5d
runc:
Version: 1.0.2
GitCommit: v1.0.2-0-g52b36a2
docker-init:
Version: 0.19.0
GitCommit: de40ad0
Docker Client 와 Docker Server 가 설치된 것을 확인할 수 있다.
Docker Client 는 실제 입력한 명령을 의미한다. 입력한 명령은 Docker Server(Host) 에 명령을 전달하고 결과를 다시 Client 에 출력하는 방식으로 상호작용한다.
Docker Desktop(=Docker Server) 이 실행되어야 Docker Client 는 명령어를 수행할 수 있다.
$ docker version
Client:
Cloud integration: v1.0.22
Version: 20.10.12
API version: 1.41
Go version: go1.16.12
Git commit: e91ed57
Built: Mon Dec 13 11:46:56 2021
OS/Arch: darwin/arm64
Context: default
Experimental: true
$ docker ps
Error response from daemon: Bad response from Docker engine

먼저 PostgreSQL 컨테이너를 생성하기 위해서 이미지를 설치해야한다.
$ docker pull postgres
Using default tag: latest
latest: Pulling from library/postgres
7ce705000c39: Pull complete
a17abc86e878: Pull complete
533f47cc37b3: Pull complete
2c171c713eb0: Pull complete
ed0c27b12f94: Pull complete
6a797b38b71e: Pull complete
04627f37bf24: Pull complete
f51bf6ebfbc0: Pull complete
28f9816e24d6: Pull complete
8639f469c02d: Pull complete
a887fdfa9c03: Pull complete
aea2cdf8abfe: Pull complete
077f421f0043: Pull complete
5cad0c187961: Pull complete
Digest: sha256:87ec5e0a167dc7d4831729f9e1d2ee7b8597dcc49ccd9e43cc5f89e808d2adae
Status: Downloaded newer image for postgres:latest
docker.io/library/postgres:latest
버전을 따로 postgres:15 처럼 명시하지 않으면 latest 한 이미지로 설치된다.
잘 설치 되었는지 확인까지 해본다.
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
postgres latest e9e3b6fe0ce3 2 months ago 457MB
이미지를 실제 작동하는 프로세스로 컨테이너화 해보자.
docker run -d \
--name postgres-container \
--restart always \
-p 5432:5432 \
-e POSTGRES_USER=user \
-e POSTGRES_PASSWORD=0000 \
-e POSTGRES_DB=postgres_db \
-v $(pwd)/postgres-data:/var/lib/postgresql/data \
postgres
-d : Detached Mode, 컨테이너를 백그라운드 형태로 실행한다.
—name : 컨테이너 이름을 postgres-container 로 설정하고
-p : Publishing Ports, [host_port]:[container_port] 형태로 작성하며 컨테이너 5432 포트를 호스트 5432 포트에 노출할 수 있다.
-e : 환경 변수를 설정한다.
-v : 도커 컨테이너는 삭제되면 내부 데이터도 함께 사라진다. Volumn Options 을 사용하면 컨테이너가 종료되어도 데이터를 볼륨에 유지시킬 수 있다. 현재 경로를 기준으로 postgres-data 라는 볼륨(디렉토리)을 생성하고 PostgreSQL 데이터베이스가 데이터를 저장하는 기본 경로인 /var/lib/postgresql/data 에 마운트한다.
생성된 컨테이너를 확인해보자.
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
60ef397d69ee postgres "docker-entrypoint.s…" 2 minutes ago Up 2 minutes 0.0.0.0:5432->5432/tcp postgres-container
postgres-container(60ef397d69ee) 컨테이너가 생성된 것을 확인할 수 있다.
생성한 도커 볼륨도 확인해보자.
$ docker volume ls
DRIVER VOLUME NAME
local postgres-data
$ docker volume inspect postgres-data
[
{
"CreatedAt": "2025-02-02T06:44:38Z",
"Driver": "local",
"Labels": null,
"Mountpoint": "/var/lib/docker/volumes/postgres-data/_data",
"Name": "postgres-data",
"Options": null,
"Scope": "local"
}
]
/var/lib/docker/volumes/postgres-data/_data 경로에 마운트한 볼륨이 생성된 것을 확인할 수 있다.
이제 컨테이너에 직접 접근해보자.
$ docker exec -it postgres-container sh
docker exec 로 실행중인 컨테이너에 접근할 수 있고 -it 옵션으로 컨테이너와 sh 셀로 통신할 수 있다.
(-i Interactive, 컨테이너와 상호작용 + -t : tty, 터미널을 할당하여 셸을 실행)
이제 설정한 사용자로 접근해서 DB 를 확인해보자.
$ psql -U user -d postgres_db
psql (17.2 (Debian 17.2-1.pgdg120+1))
Type "help" for help.
$ \l
List of databases
Name | Owner | Encoding | Locale Provider | Collate | Ctype | Locale | ICU Rules | Access privileges
-------------+-------+----------+-----------------+------------+------------+--------+-----------+-------------------
postgres | user | UTF8 | libc | en_US.utf8 | en_US.utf8 | | |
postgres_db | user | UTF8 | libc | en_US.utf8 | en_US.utf8 | | |
template0 | user | UTF8 | libc | en_US.utf8 | en_US.utf8 | | | =c/user +
| | | | | | | | user=CTc/user
template1 | user | UTF8 | libc | en_US.utf8 | en_US.utf8 | | | =c/user +
| | | | | | | | user=CTc/user
DB 를 생성해보고 데이터를 실제 생성해보자.
$ CREATE DATABASE "pgdb";
CREATE DATABASE
$ \l
List of databases
Name | Owner | Encoding | Locale Provider | Collate | Ctype | Locale | ICU Rules | Access privileges
-------------+-------+----------+-----------------+------------+------------+--------+-----------+-------------------
users | user | UTF8 | libc | en_US.utf8 | en_US.utf8 | | |
# use pgdb
\c my_database;
$ CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL
);
$ \dt
List of relations
Schema | Name | Type | Owner
--------+-------+-------+-------
public | users | table | user
$ INSERT INTO users (name, email) VALUES ('Jack', 'jack@example.com');
INSERT 0 1
$ INSERT INTO users (name, email) VALUES ('James', 'james@example.com');
INSERT 0 1
$ SELECT * FROM users;
id | name | email
----+-------+-------------------
1 | Jack | jack@example.com
2 | James | james@example.com
이제 psql 에서 나와서 컨테이너를 종료해보고 실제 생성한 데이터를 볼륨에서 확인해보자.
볼륨에서 확인할 DB 는 OID(Object Identifier) 형태로 저장되므로 생성한 DB 의 OID 를 시스템 DB 에서 먼저 확인해야한다.
$ SELECT oid, datname FROM pg_database WHERE datname='users';
oid | datname
-------+---------
16388 | users
# psql 종료
$ \q
# container exit
$ exit
$ docker inspect postgres-container | grep Mounts -A 9
"Mounts": [
{
"Type": "bind",
"Source": "~/postgres-data",
"Destination": "/var/lib/postgresql/data",
"Mode": "",
"RW": true,
"Propagation": "rprivate"
}
],
# Destination
$ ls -l ~/postgres-data/base
docker inspect 명령어는 docker volume inspect 와 다르다.
docker volume inspect 명령어는 볼륨 자체에 대한 설명만 포함할 뿐 어떤 컨테이너가 볼륨을 마운트했는지 확인 할 수 없다.
Destination 경로에 저장된 users(16388) DB 를 확인해볼 수 있다.
실습 2: Next.js 이미지 생성과 배포
앞서 실습한 예제에서는 Docker Hub 에 배포되어있는 이미지를 사용했었다.
이제 컨테이너를 실제 배포할 수 있도록 이미지를 생성하는 실습을 진행해보려고 한다.
Next.js 프로젝트를 생성한 뒤 프로젝트 루트에 Dockerfile 을 만들어준다.
# dockerfile from https://github.com/vercel/next.js/blob/canary/examples/with-docker/Dockerfile
FROM node:20-alpine AS base
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json package-lock.json* .npmrc* ./
RUN npm ci
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]
Docker Multi-stage builds 를 활용하여 이미지를 경량화할 수 있다.
각 단계(AS deps, builder, runner) 를 나누고 단계마다 필요한 파일을 이전 단계에서 --from 으로 가져온다. 최종 이미지 산출물에 불필요한 파일을 포함하지 않도록 하는 방법이다.
Docker 는 RUN COPY WORKDIR 마다 레이어를 생성하며 변경되지 않은 레이어는 캐시된다.
COPY package.json package-lock.json* .npmrc* ./
RUN npm ci
package.json package-lock.json* .npmrc* 가 변경되지 않았다면 RUN npm ci 로 생성한 node_modules 까지 캐싱될 것이다. 자주 변경될 여지가 없는 부분을 앞 단에 위치하여 빌드 속도를 개선할 수 있다.
추가로 Next.js Standalone 빌드 설정이 필요하다.
const nextConfig: NextConfig = {
output: "standalone",
}
이제 도커 명령어를 통해 Dockerfile 기반으로 이미지를 생성할 수 있다.
$ docker build -t next-app .
[+] Building 67.2s (17/17) FINISHED
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 687B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/library/node:20-alpine 2.6s
=> [base 1/1] FROM docker.io/library/node:20-alpine@sha256:2cd2a6f4cb37cf8a007d5f1e9aef090ade6b62974c7a274098c390599e8c72b4 5.2s
=> => resolve docker.io/library/node:20-alpine@sha256:2cd2a6f4cb37cf8a007d5f1e9aef090ade6b62974c7a274098c390599e8c72b4 0.0s
=> => sha256:52f827f723504aa3325bb5a54247f0dc4b92bb72569525bc951532c4ef679bd4 3.99MB / 3.99MB 0.3s
=> => sha256:2b1b01593fee8621396cac78e2488a7abf9f3d11742b595d740ba9f1e576332c 42.26MB / 42.26MB 1.7s
=> => sha256:feec47f82b2f7279e331a9b4c104eda6ef672f421e3bb3dcdcc73b513b9892dd 1.26MB / 1.26MB 0.5s
=> => sha256:2cd2a6f4cb37cf8a007d5f1e9aef090ade6b62974c7a274098c390599e8c72b4 7.67kB / 7.67kB 0.0s
=> => sha256:57070292ac3174ad96c45ea4e3265b1f9fd984608f927e5bbdf88ad73cae32ac 1.72kB / 1.72kB 0.0s
=> => sha256:4accf4954c0342fc0e93a7345998068bf33fe082c40942c4edcc9f08abcf6cad 6.20kB / 6.20kB 0.0s
=> => extracting sha256:52f827f723504aa3325bb5a54247f0dc4b92bb72569525bc951532c4ef679bd4 0.3s
=> => sha256:1ffe482b6ccf2b801e13d405db007a5370915c1f8ecdb37b77853eb791b9c1b0 443B / 443B 0.8s
=> => extracting sha256:2b1b01593fee8621396cac78e2488a7abf9f3d11742b595d740ba9f1e576332c 2.7s
=> => extracting sha256:feec47f82b2f7279e331a9b4c104eda6ef672f421e3bb3dcdcc73b513b9892dd 0.2s
=> => extracting sha256:1ffe482b6ccf2b801e13d405db007a5370915c1f8ecdb37b77853eb791b9c1b0 0.0s
=> [internal] load build context 14.0s
=> => transferring context: 337.01MB 14.0s
=> [builder 1/4] WORKDIR /app 0.1s
=> [deps 1/4] RUN apk add --no-cache libc6-compat 4.4s
=> [deps 2/4] WORKDIR /app 0.1s
=> [deps 3/4] COPY package.json package-lock.json* .npmrc* ./ 0.2s
=> [deps 4/4] RUN npm ci 11.8s
=> [builder 2/4] COPY --from=deps /app/node_modules ./node_modules 2.3s
=> [builder 3/4] COPY . . 3.8s
=> [builder 4/4] RUN npm run build 29.4s
=> [runner 2/4] COPY --from=builder /app/public ./public 0.0s
=> [runner 3/4] COPY --from=builder /app/.next/standalone ./ 0.2s
=> [runner 4/4] COPY --from=builder /app/.next/static ./.next/static 0.0s
=> exporting to image 0.3s
=> => exporting layers 0.3s
=> => writing image sha256:1c5a178a66ddabbbcb1bb2fc76507c31a65175818c7465d038117ec0c6705783 0.0s
=> => naming to docker.io/library/next-app 0.0s
도커 이미지가 정상적으로 생성되었는지 확인해보고 컨테이너까지 생성해보자.
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
next-app latest 1c5a178a66dd 43 seconds ago 211MB
$ docker run -d -p 3000:3000 next-app
4acaa965cd84007a287f1451ef90640d637bdacbe14f6ab1b578a8d78be4f47c
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
4acaa965cd84 next-app "docker-entrypoint.s…" 36 seconds ago Up 36 seconds 0.0.0.0:3000->3000/tcp angry_wescoff
백그라운드(-d)로 컨테이너 3000 포트를 Host 3000 포트로 개방(-p)해주었다.
이제 http://localhost:3000/ 로 접속해보면 Next.js 프로젝트가 구동되는 것을 확인할 수 있다.