Synology NAS에서 Django + MariaDB + Nginx Proxy Manager 구성 가이드

Synology NAS에서 Django + MariaDB + Nginx Proxy Manager 구성 가이드
Photo by Brecht Corbeel / Unsplash

Title: Synology NAS에서 Django + MariaDB + Nginx Proxy Manager 구성 가이드

추천 Excerpt: Synology NAS에서 Docker로 Django/MariaDB를 구성하고 Nginx Proxy Manager로 도메인/SSL까지 운영 가능한 표준 배포 구성을 정리합니다.


추천 Tags: Synology, NAS, Docker, Django, MariaDB, Nginx Proxy Manager, SSL

1. 개요

Synology NAS에 Docker를 활용하여 Django, MariaDB, Nginx Proxy Manager를 연동하는 방법을 소개합니다.
이 구성은 웹 서비스 운영에 필요한 보안, 성능, 유지보수 를 모두 만족하며,
Ghost, WordPress 등 다양한 블로그/웹사이트에도 응용할 수 있습니다.


2. 프로젝트 폴더 구조

아래는 표준적인 Django 프로젝트의 폴더 트리 구조입니다.

/volume1/docker/django/
├── docker-compose.yml
├── Dockerfile
├── requirements.txt
├── mariadb/                              # MariaDB 데이터 저장 폴더
├── static/                               # collectstatic 명령어로 수집된 정적 파일 위치
│   └── admin/                            # Django 관리자 정적 파일
├── oopsproject/                          # Django 프로젝트 루트
│   ├── manage.py                         # Django 진입점 스크립트
│   ├── app/                              # Django 앱 폴더
│   │   ├── __init__.py
│   │   ├── urls.py                       # 앱 URLconf
│   │   ├── views.py                      # 앱 뷰 함수
│   │   ├── templates/                    # 앱 템플릿 폴더
│   │   │   └── home.html                 # 메인 페이지 템플릿
│   │   └── static/                       # 앱 정적 파일 (CSS, JS, 이미지 등)
│   │       └── app/
│   │           ├── css/
│   │           │   └── home.css
│   │           ├── js/
│   │           │   └── home.js
│   │           └── img/
│   │               └── favicon.ico
│   └── oopsproject/                      # Django 프로젝트 설정 폴더
│       ├── __init__.py
│       ├── settings.py                   # Django 설정 파일
│       ├── urls.py                       # 메인 URLconf
│       └── wsgi.py                       # WSGI 진입점

3. 각 파일의 전체 코드

3-1. manage.py

#!/usr/bin/env python
import os
import sys

def main():
    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'oopsproject.settings')
    try:
        from django.core.management import execute_from_command_line
    except ImportError as exc:
        raise ImportError(
            "Couldn't import Django. Are you sure it's installed and "
            "available on your PYTHONPATH environment variable? Did you "
            "forget to activate a virtual environment?"
        ) from exc
    execute_from_command_line(sys.argv)

if __name__ == '__main__':
    main()

3-2. app/urls.py

from django.urls import path
from . import views

urlpatterns = [
    path('', views.home, name='home'),
]

3-3. app/views.py

from django.shortcuts import render

def home(request):
    return render(request, 'home.html')

3-4. app/templates/home.html

{% load static %}
<!DOCTYPE html>
<html>
<head>
    <title>My Django Info</title>
    <link rel="icon" href="{% static 'app/img/favicon.ico' %}">
    <link rel="stylesheet" href="{% static 'app/css/home.css' %}">
</head>
<body>
    <div class="card">
        <div class="card-title">Project Info</div>
        <div class="info-label">현재 시간</div>
        <div class="info-value" id="current-time"></div>
        <div class="info-label">오늘 날짜</div>
        <div class="info-value" id="current-date"></div>
        <div class="info-label">올해의 주차</div>
        <div class="info-value"><span id="week-number"></span>번째 주</div>
    </div>
    <script src="{% static 'app/js/home.js' %}"></script>
</body>
</html>

3-5. app/static/app/css/home.css

body {
    background: #181a1b;
    font-family: 'Segoe UI', Arial, sans-serif;
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100vh;
    margin: 0;
}
.card {
    background: #23272f;
    border-radius: 16px;
    box-shadow: 0 4px 24px rgba(0,0,0,0.45);
    padding: 32px 40px;
    min-width: 320px;
    text-align: center;
}
.card-title {
    font-size: 1.3rem;
    font-weight: bold;
    color: #e0e7ef;
    margin-bottom: 20px;
    letter-spacing: 0.02em;
}
.info-label {
    font-size: 1.05rem;
    color: #a1a1aa;
    margin-top: 12px;
    margin-bottom: 4px;
}
.info-value {
    font-size: 1.6rem;
    color: #60a5fa;
    font-weight: 500;
}

3-6. app/static/app/js/home.js

function getWeekNumber(d) {
    d = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
    d.setUTCDate(d.getUTCDate() + 4 - (d.getUTCDay() || 7));
    const yearStart = new Date(Date.UTC(d.getUTCFullYear(),0,1));
    return Math.ceil(((d - yearStart) / 86400000 + 1)/7);
}

function updateDateTime() {
    const now = new Date();
    document.getElementById('current-time').textContent =
        now.toLocaleTimeString('ko-KR', {
            hour: '2-digit',
            minute: '2-digit',
            second: '2-digit',
            hour12: false
        });

    document.getElementById('current-date').textContent =
        now.toLocaleDateString('ko-KR', {
            year: 'numeric',
            month: 'long',
            day: 'numeric',
            weekday: 'short'
        });

    document.getElementById('week-number').textContent =
        getWeekNumber(now);
}

setInterval(updateDateTime, 1000);
window.onload = updateDateTime;

3-7. app/static/app/img/favicon.ico

  • favicon.ico 파일 을 해당 위치에 복사 (온라인에서 생성하거나, 직접 준비)

3-8. oopsproject/urls.py

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('app.urls')),
]

3-9. oopsproject/settings.py

import os
from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent

SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', 'your-insecure-secret-key')
DEBUG = False

SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_CONTENT_TYPE_NOSNIFF = True
SECURE_BROWSER_XSS_FILTER = True

ALLOWED_HOSTS = [
    '192.168.1.100',   # 예시 NAS 내부 IP
    'yourdomain.com',  # 예시 도메인
    'localhost',
    '127.0.0.1'
]

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'app',
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'whitenoise.middleware.WhiteNoiseMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'oopsproject.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [BASE_DIR / 'app/templates'],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

WSGI_APPLICATION = 'oopsproject.wsgi.application'

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'exampledb',
        'USER': 'exampleuser',
        'PASSWORD': 'examplepassword',
        'HOST': 'db',
        'PORT': '3306',
        'OPTIONS': {
            'init_command': "SET sql_mode='STRICT_TRANS_TABLES'",
            'charset': 'utf8mb4',
            'use_unicode': True,
        },
        'CONN_MAX_AGE': 300
    }
}

AUTH_PASSWORD_VALIDATORS = [
    {'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'},
    {'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator'},
    {'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'},
    {'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'},
]

LANGUAGE_CODE = 'ko-kr'
TIME_ZONE = 'Asia/Seoul'
USE_I18N = True
USE_L10N = True
USE_TZ = True

STATIC_URL = '/static/'
STATIC_ROOT = '/app/static'
STATICFILES_DIRS = []
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'

MEDIA_URL = '/media/'
MEDIA_ROOT = '/app/media'

DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'handlers': {
        'file': {
            'level': 'DEBUG' if DEBUG else 'INFO',
            'class': 'logging.FileHandler',
            'filename': '/var/log/django.log',
        },
    },
    'loggers': {
        'django': {
            'handlers': ['file'],
            'level': 'DEBUG' if DEBUG else 'INFO',
            'propagate': True,
        },
    },
}

3-10. oopsproject/wsgi.py

import os
from django.core.wsgi import get_wsgi_application

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'oopsproject.settings')
application = get_wsgi_application()

4. 셋팅 방법

4-1. 파일 구조 준비

  1. **프로젝트 루트(/volume1/docker/django/)**에 폴더 트리 구조를 만듭니다.
  2. 각 파일을 위 코드대로 작성 하여 배치합니다.
  3. favicon.ico 는 온라인에서 생성하거나 직접 준비하여 app/static/app/img/에 복사합니다.

4-2. Docker 및 MariaDB, Nginx Proxy Manager 설치

  1. Docker, docker-compose 를 Synology NAS에 설치합니다.
  2. MariaDB, Django, Nginx Proxy Manager 컨테이너를 위한 docker-compose.yml 을 작성합니다.
version: '3.8'
services:
  web:
    build: .
    ports:
      - "55079:55079"
    volumes:
      - ./:/app
      - /volume1/docker/django/static:/app/static
    environment:
      DJANGO_SETTINGS_MODULE: oopsproject.settings
    depends_on:
      - db
    restart: unless-stopped
    networks:
      - django_network

  db:
    image: mariadb:11.3
    volumes:
      - type: bind
        source: /volume1/docker/django/mariadb
        target: /var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: "example_root_password"
      MYSQL_DATABASE: "exampledb"
      MYSQL_USER: "exampleuser"
      MYSQL_PASSWORD: "examplepassword"
      MYSQL_ROOT_HOST: "%"
      TZ: "Asia/Seoul"
    command:
      - --character-set-server=utf8mb4
      - --collation-server=utf8mb4_unicode_ci
      - --wait_timeout=28800
    healthcheck:
      test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s
    networks:
      - django_network
    restart: unless-stopped

networks:
  django_network:
    driver: bridge
  1. Nginx Proxy Manager 컨테이너도 별도로 실행하여, https://yourdomain.com 도메인과 SSL 인증서를 적용합니다.

4-3. Django 서버 실행 및 초기화

  1. 컨테이너 실행
docker-compose up -d
 
  1. 마이그레이션 적용
docker-compose exec web python oopsproject/manage.py migrate
  1. 정적 파일 수집
docker-compose exec web python oopsproject/manage.py collectstatic --noinput
  1. 슈퍼유저(관리자) 계정 생성
docker-compose exec web python oopsproject/manage.py createsuperuser

4-4. Nginx Proxy Manager 설정

  1. Nginx Proxy Manager 웹 UI 접속
  2. Proxy Host 추가
    • Domain Names: yourdomain.com
    • Scheme: http
    • Forward Hostname/IP: 192.168.1.100 (또는 NAS IP)
    • Forward Port: 55079 (Django 포트)
  3. SSL 인증서 발급
    • Request a new SSL Certificate 선택
    • Domain Names: yourdomain.com
    • Force SSL: 활성화

5. 결과 확인

  • https://yourdomain.com 접속 시 메인 페이지(카드형 UI) 확인
  • https://yourdomain.com/secret-admin/ 접속 후 관리자 로그인 가능
  • favicon.ico 가 브라우저 탭에 정상 표시

6. 추가 팁

  • 환경변수 분리: SECRET_KEY, DB_PASSWORD 등은 환경변수로 관리
  • 에러 페이지: 404.html, 500.html 템플릿 추가 권장
  • 백업: 데이터베이스 및 소스 코드 주기적 백업