
원래 다음 작업은 트랜잭션과 Redis 쪽으로 넘어가려고 했다.
이전 글에서도 Task와 TaskEvent가 함께 변경되는 구조이기 때문에 트랜잭션 경계를 정리하고, 현재 잡아둔 캐싱 구조를 Redis와 연결해보겠다고 정리했었다.
그런데 막상 다음 작업을 시작하려고 하니 계속 마음에 걸리는 게 하나 있었다.
→ 아직 실제 도메인에 배포하지 않았다
Flowbit은 이미 기능적으로는 동작하고 있었다.
Project
Task
TaskEvent
Timeline
Analysis
Frontend UI
전부 로컬에서는 확인이 끝난 상태였다.
그래서 오늘은 계획을 조금 바꿨다.
트랜잭션과 Redis를 더 깊게 들어가기 전에, 먼저 Flowbit을 실제 서버에 올려보기로 했다.
오늘 목표는 명확했다.
→ flowbit.kr에서 Flowbit이 실제로 동작하게 만들자
이번 배포는 최대한 단순한 구조로 진행했다.
아직 운영 서비스 수준의 복잡한 인프라를 구성하는 단계는 아니기 때문에,
하나의 EC2 인스턴스 안에서 Docker Compose 기반으로 구성했다.
구조는 다음과 같다.
flowbit.kr
↓
Gabia DNS
↓
AWS Elastic IP
↓
EC2
↓
Nginx
↓
React 정적 파일
↓
/api 요청은 Spring Boot Backend로 프록시
↓
PostgreSQL / Redis
구성 요소는 다음과 같다.
AWS EC2
Elastic IP
Gabia DNS
Docker Compose
PostgreSQL
Redis
Spring Boot Backend
Nginx
React Frontend
초기 MVP 배포 단계에서는 비용과 운영 복잡도를 줄이기 위해 하나의 EC2 인스턴스 안에 모든 구성 요소를 올렸다.
다만 구조 자체는 Docker Compose 기반으로 분리해두었기 때문에, 나중에 필요하다면 PostgreSQL은 RDS로, Redis는 ElastiCache로 분리할 수 있는 형태로 확장할 수 있다.
먼저 AWS에서 EC2 인스턴스를 생성했다.
리전은 서울 리전으로 선택했고, 인스턴스 타입은 프리 티어 범위에서 사용할 수 있는 t3.micro를 선택했다.
처음에는 기본 설정 그대로 8GiB 스토리지로 생성했지만, Docker 이미지 빌드 과정에서 용량이 부족해지는 문제가 발생했다.
no space left on device
이 에러는 코드 문제가 아니라 EC2 디스크 용량이 부족해서 발생한 문제였다.
Docker 이미지와 Gradle 빌드 캐시가 생각보다 많은 용량을 사용했기 때문에, EBS 볼륨 크기를 확장해서 해결했다.
또한 t3.micro는 메모리가 작기 때문에
Gradle 빌드 중 프로세스가 종료되는 문제도 있었다.
failed to execute bake: signal: killed
이 문제는 swap 메모리를 추가해서 해결했다.
배포를 하면서 느낀 점은 로컬에서 잘 되던 작업도 서버에서는 전혀 다른 문제가 생긴다는 것이다.
코드만 문제가 되는 게 아니라 메모리, 디스크, 권한, 네트워크 설정까지 모두 배포 과정의 일부였다.
EC2에 Docker와 Docker Compose를 설치한 뒤, 기존에 사용하던 PostgreSQL과 Redis 구성을 서버에서도 실행했다.
초기 docker-compose 구성은 다음과 같았다.
services:
postgres:
image: postgres:16
container_name: flowbit-postgres
ports:
- "5433:5432"
environment:
POSTGRES_DB: flowbit
POSTGRES_USER: flowbit
POSTGRES_PASSWORD: flowbit1234
volumes:
- flowbit-postgres-data:/var/lib/postgresql/data
redis:
image: redis:7
container_name: flowbit-redis
ports:
- "6379:6379"
volumes:
flowbit-postgres-data:
PostgreSQL은 Docker volume을 사용해서 데이터를 영속화했다.
컨테이너는 언제든 삭제되고 다시 생성될 수 있기 때문에,
DB 데이터를 컨테이너 내부에만 두면 위험하다.
그래서 PostgreSQL 데이터는 volume으로 분리했다.
이후 Spring Boot 백엔드도 Docker Compose에 포함시켜
다음과 같은 형태로 확장했다.
postgres
redis
backend
컨테이너 간 통신에서는 localhost를 사용하지 않고
docker-compose의 서비스명을 사용했다.
postgres:5432
redis:6379
이 부분이 중요했다.
EC2에서 외부로 노출되는 PostgreSQL 포트는 5433이지만,
backend 컨테이너 입장에서는 같은 Docker 네트워크 안에 있기 때문에 postgres 서비스의 내부 포트인 5432로 접근해야 했다.

백엔드 프로젝트에는 Dockerfile이 없었기 때문에 서버에서 직접 Dockerfile을 생성했다.
FROM gradle:8.8-jdk21 AS builder
WORKDIR /app
COPY . .
RUN chmod +x gradlew
RUN ./gradlew clean bootJar -x test --no-daemon
FROM eclipse-temurin:21-jre
WORKDIR /app
COPY --from=builder /app/build/libs/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
처음에는 Gradle 빌드 과정에서 메모리 부족 문제가 발생했다.
그래서 --no-daemon 옵션을 추가했고, EC2에는 swap 메모리를 추가했다.
이후 다시 빌드하니 Spring Boot 백엔드 컨테이너가 정상적으로 실행되었다.
백엔드 로그를 확인했을 때 Spring Boot가 정상적으로 실행되었고, PostgreSQL 연결도 성공했다.
API도 서버 내부에서 정상적으로 응답했다.
이 시점에서 백엔드는 EC2 위에서 Docker 컨테이너로 정상 동작하는 상태가 되었다.
다음으로 Nginx를 설정했다.
Spring Boot는 8080 포트에서 실행되고 있었지만, 사용자는 flowbit.kr로 접속해야 한다.
그래서 Nginx를 앞단에 두고 정적 파일 서빙과 API 프록시를 나누었다.
구조는 다음과 같다.
/
→ React 정적 파일
/api
→ Spring Boot Backend
Nginx 설정은 다음과 같은 방향으로 잡았다.
server {
server_name flowbit.kr www.flowbit.kr;
root /var/www/flowbit;
index index.html;
location /api/ {
proxy_pass http://localhost:8080/api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location / {
try_files $uri $uri/ /index.html;
}
}
처음에는 프론트 빌드 결과물을
/home/ubuntu/flowbit/flowbit-frontend/dist 경로에서 바로 서빙하려고 했다.

확인해보니 Nginx에서 다음과 같은 에러가 발생했다.
Permission denied
Nginx가 /home/ubuntu 하위 경로의 파일에 접근하지 못해서 발생한 문제였다.
그래서 프론트 빌드 결과물을 Nginx의 정적 파일 서빙 경로로 사용하기 좋은 /var/www/flowbit으로 복사했다.
sudo mkdir -p /var/www/flowbit
sudo cp -r dist/* /var/www/flowbit/
sudo chown -R www-data:www-data /var/www/flowbit
sudo chmod -R 755 /var/www/flowbit
이후 Nginx가 정상적으로 프론트 파일을 읽을 수 있었다.
도메인은 가비아에서 구매한 도메인을 사용했다.
메인 도메인은 다음과 같다.
flowbit.kr
그리고 다음 도메인은 메인 도메인으로 리다이렉트되도록 구성했다.
flowbit.co.kr
가비아 DNS 관리 화면에서
각 도메인의 A 레코드를 EC2 Elastic IP로 연결했다.
flowbit.kr → EC2 Elastic IP
www.flowbit.kr → EC2 Elastic IP
flowbit.co.kr → EC2 Elastic IP
www.flowbit.co.kr → EC2 Elastic IP
이후 Nginx에서 flowbit.co.kr 요청은
flowbit.kr로 리다이렉트되도록 설정했다.
server {
server_name flowbit.co.kr www.flowbit.co.kr;
return 301 http://flowbit.kr$request_uri;
}
이 부분까지 설정한 뒤
flowbit.co.kr로 접속했을 때
정상적으로 flowbit.kr로 이동하는 것을 확인했다.

React 프론트엔드는 EC2에서 직접 빌드했다.
cd flowbit-frontend
npm install
npm run build
빌드 결과물은 dist 폴더에 생성되었고,이를 /var/www/flowbit 경로로 복사했다.
sudo rm -rf /var/www/flowbit/*
sudo cp -r dist/* /var/www/flowbit/
sudo systemctl reload nginx
처음 화면은 정상적으로 떴지만, API 요청에서 문제가 발생했다.

브라우저 콘솔을 확인해보니
프론트엔드가 여전히 로컬 주소를 바라보고 있었다.
http://localhost:8080/api/projects
배포 환경에서 localhost는 EC2 서버가 아니라 사용자 브라우저가 실행되는 개인 PC를 의미한다.
그래서 프론트엔드 API baseURL을 수정했다.
기존에는 다음과 같았다.
baseURL: "http://localhost:8080/api"
이를 현재 도메인 기준으로 요청하도록 변경했다.
baseURL: "/api"
수정 후 다시 빌드하고 배포하니 프론트엔드에서 실제 서버 API를 정상적으로 호출할 수 있었다.
프론트 화면은 떴지만,
프로젝트 생성 버튼을 눌렀을 때 다시 문제가 발생했다.
서버 내부에서 curl로 POST 요청을 보내면 정상적으로 데이터가 생성되었다.
curl -X POST http://localhost:8080/api/projects
또한 도메인 경유로 요청해도 정상적으로 생성되었다.
curl -X POST https://flowbit.kr/api/projects
그런데 브라우저 화면에서 요청하면 에러가 발생했다.
확인해보니 원인은 CORS였다.
브라우저에서 요청을 보낼 때는 Origin 정보가 포함되는데,
백엔드 CORS 설정에서 flowbit.kr을 허용하지 않아 막힌 것이다.
그래서 Spring Security 설정에서 flowbit.kr, www.flowbit.kr, flowbit.co.kr 등을 허용하도록 수정했다.
또한 API 테스트용 MVP 단계이기 때문에 CSRF는 비활성화하고 /api/** 요청은 허용하도록 설정했다.
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/**").permitAll()
.requestMatchers("/hello").permitAll()
.anyRequest().permitAll()
);
return http.build();
}
}
이 설정 이후 브라우저에서도 프로젝트와 작업을 정상적으로 생성할 수 있었다.
배포가 끝난 뒤, Flowbit 안에 오늘 작업을 직접 기록했다.
프로젝트 이름은 다음과 같이 만들었다.
Flowbit 실서버 배포
그리고 하위 작업은 큰 단위로 정리했다.
AWS EC2 인스턴스 생성
Docker 기반 서버 환경 구축
Nginx 및 도메인 라우팅 설정
프론트엔드 배포 및 API 연결
Flowbit 인터넷 출생신고 🎂
이 작업들을 생성하고 시작, 완료 이벤트를 남겼다.
즉, Flowbit이 자기 자신의 배포 이력을 기록하게 된 것이다.
이 부분이 가장 인상적이었다.
단순히 배포가 끝났다는 것보다, Flowbit의 핵심인 이벤트 기록 구조를 Flowbit 자신의 첫 실서버 배포 과정에 적용했다는 점에서 의미가 있었다.


오늘 배포 과정에서 생각보다 많은 문제를 만났다.
정리하면 다음과 같다.
Gradle 빌드 중 프로세스가 종료되었다.
signal: killed
→ swap 메모리를 추가하고
Gradle 빌드에 --no-daemon 옵션을 적용했다.
Docker 이미지 빌드 중 디스크가 부족했다.
no space left on device
→ EBS 볼륨 크기를 확장해서 해결했다.
Nginx가 /home/ubuntu 하위의 프론트 빌드 파일을 읽지 못했다.
Permission denied
→ 빌드 결과물을 /var/www/flowbit으로 복사하고 권한을 조정했다.
배포된 프론트가 여전히 localhost API를 호출했다.
→ baseURL을 /api로 변경했다.
브라우저에서 API 요청 시 CORS 에러가 발생했다.
Invalid CORS request
→ Spring Security CORS 설정에 실제 도메인을 추가했다.
오늘은 단순히 서버에 프로젝트를 올린 날이 아니었다.
로컬에서만 동작하던 Flowbit이 실제 도메인을 가진 웹 서비스 형태가 된 날이었다.
이전까지는
localhost
에서만 확인할 수 있었다.
하지만 이제는
flowbit.kr
에서 직접 접근할 수 있다.
배포를 하면서 느낀 것은 개발과 배포는 확실히 다른 영역이라는 점이다.
로컬에서는 보이지 않던 문제가
서버에서는 계속 나타났다.
메모리
디스크
권한
도메인
프록시
CORS
보안 설정
이 모든 것이 맞아야
사용자가 보는 화면이 정상적으로 동작한다.
오늘은 그 과정을 직접 겪었다.
현재 Flowbit은 다음 상태까지 완료되었다.
AWS EC2 배포 완료
Elastic IP 연결 완료
Gabia DNS 연결 완료
flowbit.kr 메인 도메인 연결 완료
flowbit.co.kr → flowbit.kr 리다이렉트 설정 완료
Docker Compose 기반 PostgreSQL / Redis / Backend 실행 완료
Nginx reverse proxy 설정 완료
React 프론트엔드 정적 배포 완료
브라우저에서 프로젝트 / 작업 생성 확인 완료
즉, Flowbit은 이제 로컬 프로젝트가 아니라
실제 도메인에서 동작하는 프로젝트가 되었다.
다음 작업은 원래 계획했던 백엔드 심화 작업으로 돌아간다.
먼저 트랜잭션 경계를 정리할 예정이다.
Flowbit에서는 Task 생성과 동시에 TaskEvent가 생성되고, 상태 변경 시에도 Task와 TaskEvent가 함께 변경된다.
이런 작업은 하나의 흐름으로 성공하거나 실패해야 하기 때문에 @Transactional을 적용해 데이터 정합성을 보장하는 구조로 개선할 예정이다.
그 다음에는 Redis를 단순히 실행하는 것에서 끝내지 않고,
Spring Cache와 연결해 실제 캐시 저장소로 사용하는 구조를 검증할 예정이다.
또한 배포 자동화를 위해 GitHub Actions 기반 CI/CD도 추가할 계획이다.
현재는 수동으로 다음 과정을 진행하고 있다.
git pull
docker compose up -d --build
frontend build
/var/www/flowbit 배포
nginx reload
앞으로는 main 브랜치에 push하면
자동으로 EC2에 배포되는 구조로 개선할 예정이다.
오늘은 Flowbit에게 꽤 중요한 날이었다.
처음에는 단순히 작업 상태 변화를 기록하는 프로젝트로 시작했지만, 이제는 실제 도메인에서 동작하는 프로젝트가 되었다.
그리고 그 첫 배포 기록을 Flowbit 안에 직접 남겼다.
Flowbit으로 Flowbit의 배포 이력을 기록했다
이 문장이 오늘 작업을 가장 잘 설명하는 것 같다.
다음부터는 다시 백엔드 구조를 단단하게 만드는 작업으로 돌아간다.
트랜잭션, Redis, 그리고 CI/CD까지.
이제 Flowbit은 단순히 만들어보는 프로젝트가 아니라
운영 가능한 형태로 조금씩 가까워지고 있다.