[개발일지 #04 | 백엔드] 가벼운 Go(Golang) API 서버 구축 & Docker 배포

카카오톡 같은 크로스플랫폼 메신저 ‘딩동(DingDong)’ 만들기 프로젝트 4탄!
이번 글에서는 우리 집 유그린 NAS의 자원을 아껴 쓰기 위해, 가볍게 돌아가는 Go(Golang) 백엔드 서버의 뼈대를 만들고 NAS의 Docker 환경에 컨테이너로 올려 봅니다. 3편에서 준비한 Caddy를 통해 실제 도메인(HTTPS)으로 접속하는 것까지 확인합니다.


1. 왜 Node.js나 Python 대신 ‘Go(Golang)’를 선택했을까요?

개인용 NAS나 홈서버에서 24시간 백엔드 서버를 띄울 때 가장 신경 쓰이는 것은 메모리(RAM) 사용량입니다.

  • 가벼운 실행 환경: Go는 코드를 하나의 실행 파일(단일 바이너리)로 컴파일하므로, 별도의 런타임(Node.js, Python 인터프리터, JVM)이 필요 없습니다. 일반적으로 같은 기능을 하는 서버를 Node.js·Python·Java(Spring)로 만들 때보다 기본 메모리를 적게 쓰는 편입니다. (정확한 수치는 코드와 구성에 따라 크게 달라지므로, 9장에서 우리 서버의 실제 사용량을 직접 확인해 봅니다.)
  • 동시성 처리에 강함 (고루틴, Goroutine): 메신저는 많은 사용자가 WebSocket 연결을 오래 유지해야 합니다. Go의 경량 스레드인 고루틴은 많은 연결을 적은 자원으로 다루기에 적합합니다. 가족·친구 규모에서는 자원이 넉넉하게 남습니다.
  • 작은 Docker 이미지: 실행에 필요한 라이브러리가 실행 파일 하나에 모두 들어가므로, alpine 리눅스 같은 작은 베이스 이미지 위에 올리면 이미지 크기를 작게 만들 수 있습니다. (빌드 후 docker images로 크기를 직접 확인할 수 있습니다.)

2. Go 백엔드 핵심 아키텍처 & 기술 스택

이 시리즈에서 사용할 핵심 부품들입니다. 전부 지금 쓰는 것은 아니고, 편마다 필요할 때 하나씩 추가합니다.

flowchart LR
    classDef nodeStyle fill:#1e293b,stroke:#475569,stroke-width:2px,color:#ffffff;
    classDef dbStyle fill:#0f172a,stroke:#0284c7,stroke-width:2px,color:#ffffff;

    A["스마트폰 앱 (Flutter)"]:::nodeStyle -->|"REST / WebSocket"| B["Chi 라우터 & Gorilla WS"]:::nodeStyle
    B --> C["비즈니스 로직 계층 (Service)"]:::nodeStyle
    C -->|"Type-safe 쿼리"| D["sqlc & pgx 드라이버"]:::nodeStyle
    D --> E[(PostgreSQL 16)]:::dbStyle
    C -->|"푸시 발송 (추후)"| G["FCM / APNs"]:::nodeStyle
    C -->|"토큰 발급 (추후)"| H["LiveKit Go SDK"]:::nodeStyle
부품역할사용하는 편
go-chi/chiGo 표준 net/http와 호환되는 가볍고 빠른 HTTP 라우터이번 편
go-chi/corsCORS 헤더 처리이번 편
gorilla/websocket채팅방 브로드캐스트, 실시간 이벤트 중계채팅 편
sqlc + pgx순수 SQL을 작성하면 타입 안전한 Go 코드를 생성 (ORM 없이 사용)5편
golang-migrateSQL 마이그레이션 파일을 순서대로 적용하고 버전을 관리5편
golang-jwt로그인 토큰(JWT) 발급·검증6편
LiveKit Go SDK통화용 토큰 발급통화 편

📌 푸시 알림과 부재중 전화 타이머는? 1편 설계대로 우선 Go의 고루틴·채널 기반 워커로 시작합니다. 서버가 여러 대가 되거나 재시작 중에도 작업을 잃으면 안 되는 상황이 오면, 그때 Redis와 작업 큐(asynq)를 추가합니다. 지금은 쓰지 않으므로 Redis 컨테이너도 띄우지 않습니다.


3. Mac 개발 환경에 Go 및 도구 설치하기

내 컴퓨터(Mac)에서 Go 코드를 작성하고 테스트할 수 있도록 기본 도구를 설치합니다.

3.1 Go 컴파일러 설치

터미널을 열고 Homebrew로 Go를 설치합니다.

# Go 설치
brew install go

# 설치 확인
go version

3.2 SQL 코드 생성기(sqlc) 및 마이그레이션 도구 설치

두 도구는 5편에서 사용합니다. 지금 설치해도 되고, 5편에서 설치해도 됩니다.

# SQL 코드를 Go 코드로 변환해주는 sqlc 설치
brew install sqlc

# DB 마이그레이션 도구 설치
brew install golang-migrate

4. 백엔드 프로젝트 초기화 (server/)

내 맥(Mac)의 작업 폴더 아래에 server 디렉토리를 만들고 Go 모듈을 생성합니다.

# 1. server 디렉토리 생성 및 이동
mkdir -p server
cd server

# 2. Go 모듈 초기화
go mod init github.com/yourname/dingdong-server

# 3. 이번 편에서 사용하는 라이브러리만 설치
go get github.com/go-chi/chi/v5
go get github.com/go-chi/cors

⚠️ github.com/yourname/dingdong-server는 예시 모듈 이름입니다. 본인의 GitHub 아이디나 원하는 이름으로 바꿔도 되지만, 이후 편의 코드(import "github.com/yourname/dingdong-server/internal/...")에서도 같은 이름을 써야 합니다. 한 번 정하면 끝까지 같은 이름을 사용하세요.

💡 DB 라이브러리(pgx), JWT, WebSocket 같은 나머지 라이브러리는 해당 코드를 작성하는 편에서 설치합니다. 코드에서 쓰지 않는 라이브러리를 미리 설치해도 go mod tidy를 실행하면 목록에서 지워지기 때문입니다.


5. 클린 아키텍처 폴더 구조

복잡하지 않으면서도 유지보수가 쉬운 Go의 일반적인 폴더 구성을 사용합니다.

server/
 ├─ cmd/
 │   └─ api/
 │       └─ main.go            # 서버 진입점 (이번 편)
 ├─ internal/
 │   ├─ config/                # 환경변수 로더
 │   ├─ database/              # PostgreSQL 연결 풀 (pgx)
 │   ├─ auth/                  # JWT 발급, 검증, 미들웨어
 │   ├─ user/                  # 회원가입, 프로필, 연락처 매칭
 │   ├─ chat/                  # 채팅방, 메시지, WebSocket 허브
 │   ├─ call/                  # LiveKit 통화 시그널링
 │   └─ push/                  # FCM / APNs 푸시 작업
 ├─ migrations/                # DB 테이블 생성용 .sql 파일 (5편)
 ├─ sqlc.yaml                  # sqlc 설정 파일 (5편)
 ├─ Dockerfile                 # 유그린 NAS 배포용 멀티스테이지 Dockerfile (이번 편)
 ├─ .dockerignore              # 이미지 빌드에서 제외할 파일 목록 (이번 편)
 ├─ go.mod
 └─ go.sum

※ 위 폴더 구조는 이번 시리즈를 진행하며 단계별로 하나씩 채워 나갈 전체 설계도입니다. 지금 단계에서는 실행에 꼭 필요한 cmd/api/main.go 파일부터 만들어 보겠습니다.


6. 첫 번째 헬스체크 API 만들기 (cmd/api/main.go)

server 폴더 안에 cmd/api 폴더를 만들고 main.go 파일을 생성합니다. 서버가 잘 작동하는지 확인하기 위한 기본 HTTP 서버와 /health 엔드포인트 코드입니다.

mkdir -p cmd/api

server/cmd/api/main.go 작성:

package main

import (
	"context"
	"encoding/json"
	"errors"
	"log"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"

	"github.com/go-chi/chi/v5"
	"github.com/go-chi/chi/v5/middleware"
	"github.com/go-chi/cors"
)

func main() {
	port := os.Getenv("PORT")
	if port == "" {
		port = "3000"
	}

	r := chi.NewRouter()

	// 1. 공통 기반 미들웨어
	r.Use(middleware.RequestID)
	// RealIP는 X-Forwarded-For 같은 헤더의 값을 그대로 믿습니다.
	// 우리 구성에서는 api 컨테이너가 호스트 포트를 공개하지 않고 Caddy를 통해서만 접근되므로 괜찮지만,
	// 서버 포트를 인터넷에 직접 열게 되면 클라이언트 IP를 속일 수 있으니 이 미들웨어를 다시 검토해야 합니다.
	r.Use(middleware.RealIP)
	r.Use(middleware.Logger)
	r.Use(middleware.Recoverer)

	// 2. CORS 설정 (개발 단계용으로 느슨하게 열어 둔 설정입니다)
	// 모바일/데스크톱 네이티브 앱은 CORS의 영향을 받지 않습니다.
	// 나중에 웹 클라이언트를 만들게 되면 허용할 도메인만 지정하도록 좁혀야 합니다.
	r.Use(cors.Handler(cors.Options{
		AllowedOrigins:   []string{"https://*", "http://*"},
		AllowedMethods:   []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
		AllowedHeaders:   []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
		ExposedHeaders:   []string{"Link"},
		AllowCredentials: false,
		MaxAge:           300,
	}))

	// 3. 일반 REST API 라우터 그룹 (처리 시간 제한 60초 적용)
	// 나중에 대용량 파일 업로드 경로를 만들 때는 이 그룹과 분리해서 제한 시간을 따로 줍니다.
	r.Group(func(api chi.Router) {
		api.Use(middleware.Timeout(60 * time.Second))

		api.Get("/health", func(w http.ResponseWriter, r *http.Request) {
			res := map[string]string{
				"status":  "ok",
				"server":  "dingdong-go",
				"time":    time.Now().Format(time.RFC3339),
				"version": "v0.1.0",
			}
			w.Header().Set("Content-Type", "application/json")
			w.WriteHeader(http.StatusOK)
			json.NewEncoder(w).Encode(res)
		})
	})

	// 4. 실시간 WebSocket 엔드포인트 그룹
	// WebSocket은 연결을 오래 유지해야 하므로, 제한 시간(Timeout) 미들웨어가 없는 그룹에 둡니다.
	// 지금은 자리만 잡아 둔 코드입니다. 실제로 구현할 때는 반드시 로그인 인증(6편의 JWT 미들웨어)을 붙입니다.
	r.Group(func(ws chi.Router) {
		ws.Get("/ws", func(w http.ResponseWriter, r *http.Request) {
			w.Write([]byte("WebSocket 엔드포인트 (추후 구현)"))
		})
	})

	// 5. 서버 설정 및 Graceful Shutdown (우아한 종료)
	server := &http.Server{
		Addr:    ":" + port,
		Handler: r,
		// 요청 헤더를 읽는 시간만 제한합니다. (느린 연결로 서버를 붙잡아 두는 공격을 줄여 줍니다.)
		ReadHeaderTimeout: 10 * time.Second,
		// ReadTimeout과 WriteTimeout은 일부러 0(제한 없음)으로 둡니다.
		// 이 값은 요청 본문을 다 읽는 시간까지 제한하기 때문에, 느린 모바일 회선에서
		// 대용량 사진/파일(최대 310MB)을 올리면 중간에 끊길 수 있습니다.
		// 일반 REST API의 처리 시간은 위의 chi middleware.Timeout으로 제어합니다.
		ReadTimeout:  0,
		WriteTimeout: 0,
		IdleTimeout:  60 * time.Second,
	}

	// 백그라운드 고루틴으로 서버 리슨
	go func() {
		log.Printf("🚀 딩동 Go 백엔드 서버가 포트 %s에서 시작되었습니다!", port)
		if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
			log.Fatalf("서버 시작 실패: %v", err)
		}
	}()

	// 종료 시그널(Ctrl+C, SIGTERM 등) 대기
	quit := make(chan os.Signal, 1)
	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
	<-quit
	log.Println("서버 종료 신호를 감지했습니다. 안전하게 종료합니다...")

	// 최대 10초 대기하며 진행 중인 작업을 마무리한 뒤 종료
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()
	if err := server.Shutdown(ctx); err != nil {
		log.Printf("종료 중 에러: %v", err)
	}
	log.Println("서버가 종료되었습니다.")
}

로컬 실행 테스트

코드 작성이 끝났으면 패키지 의존성을 정리(go mod tidy)한 뒤 바로 실행해 봅니다.

# 1. 현재 코드에 맞게 go.mod / go.sum 정리 (필수! Dockerfile이 go.sum 파일을 사용합니다)
go mod tidy

# 2. 서버 실행
go run ./cmd/api

다른 터미널 창을 열고 헬스체크 요청을 보내봅니다.

curl http://localhost:3000/health

응답 결과:

{
  "server": "dingdong-go",
  "status": "ok",
  "time": "2026-10-29T15:00:00+09:00",
  "version": "v0.1.0"
}

💡 서버를 끌 때는 실행 중인 터미널에서 Ctrl + C를 누르면 “안전하게 종료합니다” 로그가 나옵니다. address already in use 에러가 나면 이미 3000번 포트를 쓰는 프로그램이 있는 것이니 PORT=3001 go run ./cmd/api처럼 다른 포트로 실행해 보세요.


7. 유그린 NAS 배포용 Dockerfile 만들기

7.1 Dockerfile이 도대체 무슨 의미인가요? (쉽게 이해하기)

방금 6번에서는 내 맥에서 go run 명령어로 서버를 띄웠습니다. 하지만 우리의 최종 목표는 맥을 끄더라도 집에 있는 유그린 NAS에서 24시간 내내 이 서버가 돌아가게 만드는 것입니다.

그런데 유그린 NAS에는 Go 언어나 컴파일러가 깔려 있지 않습니다. 그래서 **”이 서버 프로그램을 실행할 수 있는 작은 포장 박스(Docker 컨테이너)”**를 만들어야 하는데, Dockerfile이 바로 그 포장 박스를 만드는 **’조립 설명서’**입니다.

  • 1단계 (Builder 작업실): “Go가 설치된 임시 작업실에서 우리가 짠 코드를 실행 파일(server) 하나로 구워낸다!”
  • 2단계 (가벼운 도시락통): “다 구웠으면 무거운 Go 개발 도구들은 버리고, 구워진 실행 파일 하나만 아주 가벼운 도시락통(alpine 리눅스)에 쏙 담는다!”

이 기법을 **멀티스테이지 빌드(Multi-stage Build)**라고 부르며, NAS에 이것저것 설치할 필요 없이 이 포장 박스 하나만 띄워 주면 서버가 돌아갑니다.

7.2 어디에, 어떻게 만드나요?

VS Code의 좌측 탐색기에서 server 폴더를 우클릭 ➡️ **[새 파일]**을 누릅니다.

파일 이름은 확장자 없이 대소문자를 정확히 지켜 **Dockerfile**이라고 입력합니다. (최종 위치: server/Dockerfile)

열린 빈 파일에 아래 코드를 붙여넣고 저장(Cmd + S)합니다.

# 1단계: 빌드 환경 (Builder 작업실)
# 태그를 고정하지 않은 이유: 맥에서 쓴 Go 버전이 go.mod에 기록되는데,
# 이 이미지의 Go가 그보다 낮으면 빌드가 실패하기 때문입니다.
# 항상 같은 결과가 필요해지면 `golang:1.xx-alpine`처럼 버전을 고정하세요.
FROM golang:alpine AS builder

# NAS의 CPU 종류(Intel/AMD 또는 ARM)에 맞춰 자동으로 빌드합니다.
ARG TARGETARCH

WORKDIR /app
RUN apk add --no-cache git ca-certificates

# 의존성 캐시 활용 (소스가 바뀌어도 라이브러리 다운로드는 다시 하지 않음)
COPY go.mod go.sum ./
RUN go mod download

# 소스코드 복사 및 단일 정적 바이너리로 컴파일
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH:-amd64} \
    go build -ldflags="-s -w" -o /app/server ./cmd/api

# 2단계: 실제 실행 환경 (가벼운 도시락통)
FROM alpine:3.22

WORKDIR /app
RUN apk add --no-cache ca-certificates tzdata \
    && adduser -D -u 10001 app
ENV TZ=Asia/Seoul

COPY --from=builder /app/server /app/server

# 보안: 관리자(root)가 아닌 일반 사용자로 실행합니다.
USER app

EXPOSE 3000

# 컨테이너 상태 확인 (docker compose ps에서 healthy / unhealthy로 표시됩니다)
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
  CMD wget -q -O /dev/null "http://127.0.0.1:${PORT:-3000}/health" || exit 1

ENTRYPOINT ["/app/server"]

같은 server 폴더에 .dockerignore 파일도 만들어, 이미지를 만들 때 필요 없는 파일이 들어가지 않게 합니다.

.git
.env
*.md
bin/

💡 이미지 크기는? 이 Dockerfile로 만든 이미지는 Node.js 기반 이미지보다 보통 훨씬 작습니다. 빌드가 끝난 뒤 docker images로 크기를 직접 확인해 보세요. (사용 중인 베이스 이미지와 코드 양에 따라 달라집니다.)

⚠️ alpine:3.22 같은 버전은 시간이 지나면 보안 업데이트 지원이 끝납니다. 몇 달에 한 번은 alpine 릴리스 목록에서 지원 중인 최신 3.x 버전으로 올려 주세요.

8. 유그린 NAS의 딩동 프로젝트에 통합하기 (docker-compose.yml)

지난 **[3편]**에서 우리는 NAS에 Caddy(/volume1/docker/caddy)와 딩동 프로젝트(/volume1/docker/dingdong)를 분리해서 구축했습니다. 이제 딩동 프로젝트 폴더에서 방금 만든 Go API 서버와 PostgreSQL을 한 파일로 합쳐서 관리합니다.

8.1 유그린 NAS의 폴더 배치 구조

맥에서 작성한 server 폴더를 NAS의 /volume1/docker/dingdong/ 안으로 복사하고, 프로젝트 루트에 통합 docker-compose.yml을 만듭니다.

/volume1/docker/dingdong/          # 딩동 프로젝트 루트 (유그린 NAS SSD)
 ├─ docker-compose.yml             # 👈 API + Postgres 통합 관리 파일
 ├─ .env                           # 👈 환경변수 통합 파일 (비밀번호 포함, 외부 공개 금지!)
 ├─ server/                        # 👈 맥에서 작성한 Go 서버 코드 복사 (Dockerfile 포함)
 │   ├─ cmd/api/main.go
 │   ├─ Dockerfile
 │   ├─ .dockerignore
 │   ├─ go.mod
 │   └─ go.sum
 └─ postgres/
     └─ data/                      # (3편에서 생성된 DB 데이터 폴더)

⚠️ 3편에서 postgres/ 폴더 안에서 DB를 이미 띄워 두셨다면, 합치기 전에 반드시 아래 순서로 진행하세요.

  1. 기존 컨테이너를 중지합니다. (데이터는 data/ 폴더에 남아 있으니 안전합니다.)cd /volume1/docker/dingdong/postgres docker compose down
  2. 헷갈리지 않도록 예전 파일 이름을 바꿔 둡니다.mv docker-compose.yml docker-compose.yml.bak
  3. 아래 8.2의 .env에는 3편에서 만든 DB 비밀번호를 그대로 넣습니다. 비밀번호는 DB를 처음 만들 때 한 번만 적용되므로, 값이 다르면 api가 접속에 실패합니다.

8.2 환경변수 파일 (/volume1/docker/dingdong/.env)

기존 DB 설정 아래에 Go 서버 포트와, 로그인 토큰 서명용 JWT_SECRET을 추가합니다.

# 1. PostgreSQL 설정 (3편과 동일한 값)
POSTGRES_DB=dingdong
POSTGRES_USER=dingdong
POSTGRES_PASSWORD=3편에서_만든_비밀번호를_그대로_입력

# 2. [4편 추가] Go 서버 설정
PORT=3000

# 3. [4편 추가] JWT 서명용 비밀키 (6편에서 사용, 최소 32자 이상 무작위 값)
JWT_SECRET=CHANGE_ME_아래_명령으로_만든_값으로_교체

💡 JWT_SECRET 만들기: NAS나 맥 터미널에서 아래 명령을 실행해 나온 값을 붙여넣으세요.

openssl rand -hex 32

이 값은 서명에 쓰이는 비밀키입니다. 유출되면 누구나 가짜 로그인 토큰을 만들 수 있으니 블로그, GitHub, 캡처 화면에 올리지 마세요. 지금 코드는 아직 사용하지 않고 6편에서 사용합니다.

⚠️ 비밀번호와 키가 들어 있는 .env 파일은 권한을 제한하고, 코드를 Git에 올린다면 .gitignore에 .env를 꼭 넣으세요.

chmod 600 /volume1/docker/dingdong/.env

8.3 완성된 /volume1/docker/dingdong/docker-compose.yml

여기서 가장 중요한 핵심은 api 컨테이너만 외부 대문인 caddy-net과 내부 DB망인 dingdong-net 양쪽에 모두 연결되어 다리 역할을 한다는 점입니다.

services:
  # 1. Go 백엔드 API 서버
  api:
    build:
      context: ./server
      dockerfile: Dockerfile
    container_name: dingdong-api        # 3편 Caddyfile에서 지정한 이름과 일치!
    restart: unless-stopped
    env_file: .env                      # PORT, JWT_SECRET 등을 컨테이너에 전달
    environment:
      # 5편부터 사용할 DB 접속 주소 (같은 Docker 네트워크 안이라 sslmode=disable)
      DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}?sslmode=disable
    depends_on:
      postgres:
        condition: service_healthy      # DB가 준비된 뒤에 시작
    networks:
      - caddy-net                       # Caddy로부터 외부 트래픽을 받기 위한 공용망
      - dingdong-net                    # 내부 DB와 통신하기 위한 전용망

  # 2. PostgreSQL 16 데이터베이스
  postgres:
    image: postgres:16-alpine
    container_name: dingdong-postgres
    restart: unless-stopped
    environment:                        # env_file 대신 필요한 값만 전달 (JWT_SECRET이 DB 컨테이너에 들어가지 않도록)
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      TZ: Asia/Seoul
    ports:
      - "127.0.0.1:5432:5432"           # NAS 자기 자신에서만 접속 가능 (맥에서는 3편의 SSH 터널 사용)
    volumes:
      - ./postgres/data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
      interval: 5s
      timeout: 5s
      retries: 10
    networks:
      - dingdong-net                    # 외부(Caddy)와 분리된 내부망만 연결

networks:
  caddy-net:
    external: true                      # 3편에서 만든 Caddy 공용 네트워크에 합류
  dingdong-net:
    name: dingdong-net                  # 딩동 내부 컨테이너끼리만 대화하는 전용 네트워크

💡 api 컨테이너에는 ports:가 없습니다. 호스트 포트를 열지 않고 Caddy(caddy-net)를 통해서만 접근되므로, 인터넷에서는 반드시 HTTPS를 거치게 됩니다.

💡 파일 확장자 팁 (.yml vs .yaml): 유그린 UGOS Pro의 Docker 앱이나 파일 관리자에서 확장자가 .yaml로 표시되더라도 걱정하지 마세요. Docker Compose는 두 확장자를 같은 형식으로 읽습니다.


9. 유그린 NAS에서 빌드 및 외부 접속 테스트!

이제 NAS 터미널(SSH)에서 딩동 폴더로 이동해 컨테이너를 빌드하고 실행합니다.

cd /volume1/docker/dingdong

# Go 서버 이미지를 빌드(--build)하면서 백그라운드로 실행
docker compose up -d --build

# 실행 상태 확인 (api와 postgres의 STATUS가 healthy면 정상)
docker compose ps

# api 로그 확인 (빠져나올 땐 Ctrl + C)
docker compose logs -f api

로그에 🚀 딩동 Go 백엔드 서버가 포트 3000에서 시작되었습니다!가 떴다면, 스마트폰이나 PC 웹 브라우저 주소창에 3편에서 설정한 내 도메인을 입력해 보세요.

  • 접속 주소: https://api.myfamily.com/health (본인 도메인으로 바꿔서 입력)
  • 결과: 3편에서는 502 Bad Gateway가 떴던 주소에서, 이제 보안 자물쇠(HTTPS)와 함께 Go 서버가 응답하는 JSON 데이터가 보입니다.

내 서버는 얼마나 가벼울까? 직접 확인해 보기

앞에서 “가볍다”고 했으니 실제 수치를 확인해 봅니다.

# 컨테이너별 현재 메모리/CPU 사용량 (한 번만 출력)
docker stats --no-stream

# 이미지 크기 확인
docker images

dingdong-api의 MEM USAGE 값이 우리 서버의 실제 메모리 사용량입니다. 기능이 늘어나면 이 값도 늘어나므로, 편이 쌓일 때마다 한 번씩 확인해 보면 재미있습니다.

🔧 접속이 안 될 때 체크리스트

  1. docker compose ps에서 dingdong-api가 healthy인가? (아니라면 docker compose logs api)
  2. 3편의 Caddy 컨테이너가 떠 있고, Caddyfile의 reverse_proxy dingdong-api:3000과 컨테이너 이름이 같은가?
  3. api가 caddy-net에 연결되어 있는가? (docker network inspect caddy-net에서 dingdong-api 확인)
  4. 같은 DB를 쓰는데 .env의 POSTGRES_PASSWORD가 3편에서 만든 값과 다르지는 않은가?

10. 다음 편 예고

이제 유그린 NAS 위에서 Go 백엔드 API 서버가 Caddy(HTTPS), PostgreSQL과 함께 돌아가고 있습니다.

다음 **[개발일지 #05 | 백엔드]**에서는 golang-migrate와 sqlc를 사용해 메신저의 핵심 데이터베이스 스키마(회원 정보, 전화번호 해시, 채팅방, 메시지 순번)를 작성하고, 타입 안전한 SQL 쿼리 코드를 자동으로 생성하는 과정을 실습 위주로 다루겠습니다!

📂 Part 1. 기획 & 아키텍처 설계

📂 Part 2. 홈서버 & 인프라 구축

📂 Part 3. 백엔드(API) & 실시간 통화 서버

📂 Part 4. 모바일 앱 개발 (iOS / Android)

  • ✅ [개발일지 #07 | 클라이언트] Flutter 앱 첫 연동: Secure Storage 토큰 관리, Dio 자동 갱신 & 로그인 화면 구현
  • ⏳ [#09 | 클라이언트] 친구 목록 동기화 & 채팅방 목록 UI 연동하기 (예정)
  • ⏳ [#11 | 클라이언트] 실시간 WebSocket 채팅방 화면 & 끊김 없는 대화 동기화 구현 (예정)
  • ⏳ [#12 | 앱-공통] 300MB 대용량 원본 사진/영상 전송 및 미디어 처리 (예정)
  • ⏳ [#13 | 앱-푸시] 앱이 꺼져 있어도 울리는 푸시 알림(FCM/APNs) 및 보이스톡 연동 (예정)

📂 Part 5. 정식 배포 & 운영

  • ⏳ [#14 | 배포] 백엔드 서버 도커 이미지 빌드 및 유그린 NAS 정식 배포 (예정)
  • ⏳ [#15 | 운영] DB 자동 백업 스크립트 구성 및 가족 실사용 후기 (예정)

댓글 남기기