Skip to content

Deployment Guide

2026-08-12 — 배포 방식이 바뀌었습니다. 이미지는 Mac 에서 빌드해 서버로 밀어 넣고, 서버는 절대 빌드하지 않습니다. 이 문서 최상단 두 절이 현재 기준이며, 그 아래 production_deploy.sh 관련 내용은 **과거 기록**입니다 (해당 스크립트는 더 이상 쓰지 않습니다).

현재 배포 방식 (Mac 빌드 + 이미지 전송)

./scripts/deploy.sh staging      # → CT 133 (ext-stg-project-c), branch develop
./scripts/deploy.sh production   # → infra-production, branch main

흐름: 변경된 서비스만 linux/amd64 로 빌드 → 정말 amd64 인지 검증 → docker save | ssh docker load → 서버에 이미지가 있는지 검증 → git pull + up -d(--build 없음) → 빌드 캐시는 Mac 에서 정리.

Odoo 모듈 업그레이드는 **별도 단계**입니다. 배포가 대신 해주지 않습니다.

ENV=staging    ./scripts/deploy.sh migrate     # 기본 MODULES=jdx_crm_saas
ENV=production ./scripts/deploy.sh migrate

사전 pg_dump → crm_lead_sns_trigger / contact_update_trigger 비활성화 → 업그레이드 → 재활성화 → Odoo 재기동 순입니다. 두 트리거는 행마다 발화하므로 켜 둔 채 업그레이드하면 SNS 알림이 폭주합니다.

왜 바꿨나: 서버 빌드는 스테이징 CT 133 에 19GB 의 빌드 캐시를 쌓아 40G 디스크를 84% 까지 채웠고(2026-08-12), 프로덕션은 배포마다 2GB Odoo 이미지를 직접 빌드하고 있었습니다.

효과 (CT 133 실측): 전환 배포 직후 서버 빌드 캐시가 5.708GB 에서 단 1바이트도 늘지 않았습니다(54개 항목 그대로). 이것이 서버가 빌드하지 않았다는 직접 증거입니다. 이후 구 캐시를 제거해 0B, 디스크 38% → 30%.

규칙

  • 서버에서 docker compose up 을 직접 실행하지 마십시오 — ~/jdx/deploy.sh 가 Infisical 시크릿을 주입합니다
  • 서버에 --build 를 넘기지 마십시오. 이미지가 없으면 보내십시오. deploy.sh 는 서버가 몰래 빌드하게 두느니 중단합니다
  • extra-addons/ 는 bind-mount 라 git pull 만으로 서버에 도달합니다 — 이미지 재빌드가 필요 없습니다. 다만 모델·뷰 변경은 여전히 migrate 가 필요합니다
  • 이 Mac 은 arm64 라 amd64 빌드는 에뮬레이션입니다. 마커가 없는 첫 배포는 전부 빌드하므로 느립니다

롤백 (서버 빌드로 되돌리기)

build: 섹션을 compose 에 남겨 뒀으므로 이 경로는 항상 살아 있습니다.

ssh infra-production 'cd ~/jdx && git pull origin main && ~/jdx/deploy.sh --build'

전환 중 겪은 빌드 실패 4건 (2026-08-12)

스테이징 첫 배포는 네 번 만에 성공했습니다. 넷 다 **서버에서 빌드할 때는 드러나지 않던 문제**입니다. 서버는 매번 깨끗한 체크아웃에서 빌드하지만 개발 머신에는 로컬 산출물이 있고, 아키텍처도 다릅니다. 전환의 비용이 여기 있었습니다.

① ai-visualizer — apt 캐시 마운트 id 충돌

E: Could not get lock /var/lib/apt/lists/lock. It is held by process 0
exit code: 100

한 RUN 안에서 /var/cache/apt 와 /var/lib/apt 에 같은 cache id 를 걸고 있었습니다. 두 경로가 같은 디렉토리가 되어 apt 가 자기 lists 를 잠그지 못합니다.

수정: ai-visualizer/docker/Dockerfile — id 를 apt-cache-* / apt-lib-* 로 분리. 경로가 겹치는 것 자체가 잘못이므로 서버에서 통과했더라도 버그입니다.

② nextjs — .dockerignore 부재로 로컬 node_modules 충돌

cannot copy to non-directory: .../node_modules/@eslint/config-array

Dockerfile 이 COPY --from=deps /app/node_modules 다음에 COPY . . 를 합니다. .dockerignore 가 없어 로컬 node_modules 1.1GB 가 그 위에 덮이는데, pnpm 의 심볼릭 트리와 실제 디렉토리가 충돌합니다. 서버에는 node_modules 가 없으니 평생 드러나지 않던 문제입니다. 덤으로 .next 491MB 까지 컨텍스트로 올라가고 있었습니다.

수정: nextjs-frontend/.dockerignore 신설.

③ fastapi — 같은 원인, 조용한 버전

fastapi-backend/Dockerfile 도 COPY --chown=appuser:appuser . . 로 끝납니다. 빌드는 성공하지만 arm64 로 만든 .venv 245MB 와 mypy 캐시 68MB 가 amd64 이미지 안으로 그대로 들어가고 있었습니다. 실패하지 않았기에 더 위험합니다.

수정: fastapi-backend/.dockerignore 신설.

④ nextjs — arm64 이미지가 만들어짐 (가드가 잡음)

[error ] jdx-nextjs:latest is 'arm64', expected amd64 — aborting before ship

DOCKER_DEFAULT_PLATFORM=linux/amd64 를 줬는데도 compose 가 nextjs 에만 platform 을 넣지 않았습니다. docker compose build --print 로 확인한 결과 fastapi·odoo·docs·landing-mb·ai-visualizer 에는 platforms: [linux/amd64] 가 들어가고 nextjs 에는 없었습니다(nextjs 는 profiles 를 가지고 있어 다른 경로를 탑니다).

deploy.sh 의 verify_amd64 가 ship 직전에 잡아서 사고는 없었지만, 가드에 기대는 것은 설계가 아닙니다.

수정: docker-compose.staging.yml / docker-compose.prod.yml 의 빌드 대상 13곳에 platforms: [linux/amd64] 명시. 서버가 amd64 라는 사실은 환경변수가 아니라 compose 에 적혀 있어야 합니다.

⑤ (실패는 아님) migrate 의 트리거 복구 트랩이 죽어 있었음

배포 도중 발견했습니다. 트리거 재활성화 안전망을 trap ... ERR 로 걸어 뒀는데, 이 스크립트의 모든 실패 경로는 ... || err "..." 를 거치고 err() 는 exit 를 호출합니다. exit 는 ERR 트랩을 발화시키지 않습니다 (게다가 || 자체가 ERR 을 억제합니다). 즉 업그레이드가 실패하면 트리거가 꺼진 채로 남고, CRM 쓰기가 SNS 에 도달하지 않는데 아무 신호도 없었을 것입니다.

수정: EXIT 트랩 + TRIGGERS_DISABLED 플래그로 교체.

배운 것

  • 로컬 빌드 산출물은 빌드 컨텍스트의 일부다. COPY . . 로 끝나는 Dockerfile 에 .dockerignore 가 없으면, 개발 머신에서 빌드하는 순간 드러납니다. 새 서비스를 추가할 때 함께 확인하십시오
  • DOCKER_DEFAULT_PLATFORM 을 신뢰하지 마십시오. compose 가 서비스별로 조용히 무시합니다. docker compose build --print 로 실제 bake 정의를 보면 확인할 수 있습니다
  • 셸 안전망은 실제로 발화하는지 확인하십시오. set -e + ERR 트랩은 || err 패턴과 함께 쓰면 죽은 코드가 됩니다

이하 과거 기록 — production_deploy.sh 시대

⚠️ 아래는 더 이상 현행이 아닙니다. production_deploy.sh 는 쓰지 않고, SSL 은 Traefik 이, 시크릿은 Infisical 이 담당하며, 서버에 .env 를 두지 않습니다. 당시 절차를 기록으로만 남깁니다.

Deployment Script Overview

All deployment operations use ./production_deploy.sh, which reads configuration from .env.

# View current configuration
./production_deploy.sh config

# View all available commands
./production_deploy.sh help

Quick Reference

Command Description
deploy Full deployment (backup, pull, build, update modules)
update Quick update (backup, pull, rebuild, update modules)
start Start all services
stop Stop all services
restart Restart all services
status Check service status
logs View logs
init-ssl Full SSL setup
config Show current configuration

Environments

Environment Config Database Exposed Ports
Test docker-compose.test.yml Local PostgreSQL All
Production docker-compose.prod.yml AWS RDS Only 80, 443

Test Environment Deployment

Prerequisites

  • Docker and Docker Compose installed
  • Git access to repository
  • Ports available: 80, 443, 8016, 8000, 8001, 8002

Steps

# 1. Clone repository
git clone git@github.com:yourorg/odoo15-production.git
cd odoo15-production

# 2. Configure environment
cp .env.example .env
nano .env  # Verify settings

# 3. Verify configuration
./production_deploy.sh config

# 4. Start services
./production_deploy.sh start

# 5. Verify services
./production_deploy.sh health

Access URLs (Test)

Service URL
Odoo http://localhost:8016
Landing Page http://localhost
PWA http://localhost:8000
Docs http://localhost:8002

Production Environment Deployment

Prerequisites

  • AWS EC2/Lightsail instance
  • AWS RDS PostgreSQL instance
  • Domain names configured (DNS pointing to server)
  • Ports 80, 443 open in security group

Step 1: Initial Setup

# SSH to production server
ssh user@production-server

# Clone repository
git clone git@github.com:yourorg/odoo15-production.git
cd odoo15-production

# Create production .env
cp .env.example .env
nano .env

Step 2: Configure .env for Production

# Environment
ENVIRONMENT=production
GIT_BRANCH=main

# Docker Compose (use production overlay)
COMPOSE_FILE=docker-compose.yml:docker-compose.prod.yml

# Database (AWS RDS)
DB_HOST=your-db.xxxxx.us-east-1.rds.amazonaws.com
DB_PORT=5432
DB_NAME=odoo_production
DB_USER=odoo_admin
DB_PASSWORD=<strong-password>

# Domains
DOMAIN_LANDING=yourcompany.com
DOMAIN_ERP=erp.yourcompany.com
DOMAIN_PWA=pwa.yourcompany.com
DOMAIN_DOCS=docs.yourcompany.com

# SSL
LETSENCRYPT_EMAIL=admin@yourcompany.com

Step 3: SSL Certificate Setup

# Full SSL setup (recommended)
./production_deploy.sh init-ssl

This runs the following automatically:

  1. Creates dummy self-signed certificates
  2. Generates nginx config with your domains
  3. Starts nginx with HTTPS
  4. Requests real certificates from Let's Encrypt

Manual SSL Steps (if needed)

# Step-by-step SSL setup
./production_deploy.sh init-ssl-dummy      # 1. Create dummy certs
./production_deploy.sh configure-nginx     # 2. Generate nginx config
./production_deploy.sh restart             # 3. Start services
./production_deploy.sh init-ssl-request    # 4. Get real certs

Step 4: Deploy Application

# Start all services
./production_deploy.sh start

# Or full deployment (includes module updates)
./production_deploy.sh deploy

Step 5: Setup SSL Auto-Renewal

# Add to root crontab
sudo crontab -e

# Add this line (runs every Sunday at 3 AM):
0 3 * * 0 /path/to/odoo15-production/scripts/ssl-renew-cron.sh >> /var/log/ssl-renew.log 2>&1

Deployment Commands

Full Deployment

Use for major updates or initial deployment:

./production_deploy.sh deploy

This performs:

  1. Check .env exists
  2. Create backup
  3. Pull latest code from GIT_BRANCH
  4. Stop containers
  5. Build Docker images
  6. Start services
  7. Update all Odoo modules
  8. Run health checks

Quick Update

Use for routine updates:

./production_deploy.sh update

This performs:

  1. Check .env exists
  2. Create backup
  3. Pull latest code
  4. Rebuild containers (if needed)
  5. Restart services
  6. Update Odoo modules
  7. Run health checks

Update Single Module

./production_deploy.sh update-module justcall_sms

Service Management

Start/Stop/Restart

# All services
./production_deploy.sh start
./production_deploy.sh stop
./production_deploy.sh restart

# Single service
./production_deploy.sh restart-service odoo
./production_deploy.sh restart-service nginx
./production_deploy.sh restart-service pwa

View Status

./production_deploy.sh status

View Logs

# All services
./production_deploy.sh logs

# Specific service
./production_deploy.sh logs odoo
./production_deploy.sh logs nginx

Rebuild Services

# Rebuild all
./production_deploy.sh rebuild-all

# Rebuild specific service
./production_deploy.sh rebuild-pwa
./production_deploy.sh rebuild-docs
./production_deploy.sh rebuild-landing

SSL Commands

Command Description
init-ssl Full SSL setup (dummy → configure → request)
init-ssl-dummy Create self-signed dummy certificates
init-ssl-request Request real certificates from Let's Encrypt
renew-ssl Renew existing certificates
configure-nginx Generate nginx config with domains from .env

SSL Certificate Locations

nginx/ssl/
└── live/
    └── yourcompany.com/
        ├── fullchain.pem    # Certificate chain
        └── privkey.pem      # Private key

Manual Certificate Renewal

./production_deploy.sh renew-ssl

Database Management

Access Database Shell

./production_deploy.sh db-shell

Odoo Shell

./production_deploy.sh odoo-shell

Install/Update Module

# Via deploy script
./production_deploy.sh update-module module_name

# Or directly
docker compose exec odoo odoo -u module_name --stop-after-init -d $DB_NAME

Backup & Recovery

Create Backup

./production_deploy.sh backup

This runs ./scripts/backup.sh full which backs up:

  • Database (pg_dump)
  • Filestore (./odoo-data)
  • Configuration files

Restore from Backup

./scripts/restore.sh <backup-file>

Deployment Checklist

Pre-Deployment

  • Code reviewed and tested
  • .env configured correctly
  • Database backup taken (./production_deploy.sh backup)
  • Team notified

Deployment

  • Run deployment command
  • Verify health checks pass
  • Check logs for errors

Post-Deployment

  • Test critical functionality
  • Monitor logs for 15 minutes
  • Verify backups running
  • Update documentation if needed

Rollback Procedure

If deployment fails:

# 1. Stop services
./production_deploy.sh stop

# 2. Restore previous code
git checkout <previous-tag>

# 3. Restore database if needed
./scripts/restore.sh <backup-file>

# 4. Start services
./production_deploy.sh start

# 5. Verify rollback
./production_deploy.sh health

Troubleshooting

View Configuration

./production_deploy.sh config

Service Won't Start

# Check logs
./production_deploy.sh logs <service>

# Check container status
./production_deploy.sh status

# Force recreate
docker compose up -d --force-recreate <service>

Database Connection Issues

# Test database connection
docker compose exec db pg_isready -U odoo

# Check database exists
docker compose exec db psql -U odoo -l

SSL Certificate Issues

# Test nginx config
docker compose exec nginx nginx -t

# Check certificate files exist
ls -la nginx/ssl/live/*/

# Request new certificates
./production_deploy.sh init-ssl-request

Nginx Issues

# Test config
docker compose exec nginx nginx -t

# Reload config
docker compose exec nginx nginx -s reload

# View nginx logs
./production_deploy.sh logs nginx

File Reference

File Purpose
production_deploy.sh Main deployment script
scripts/ssl-init.sh SSL certificate management
scripts/ssl-renew-cron.sh SSL auto-renewal (cron)
scripts/backup.sh Backup script
scripts/restore.sh Restore script
scripts/health-check.sh Health check script
nginx/conf.d/default.conf Active nginx config
nginx/conf.d/production.conf.template Production HTTPS template