This open source project offer docker that three kind of web or API service solutions by php, gunicorn, uwsgi based on nginx server. You can easily create custom configuration files for nginx using a shell script. Supports https and certbot auto-extension script. there are default security settings in the nginx config file. docker-compose allows you to easily install and operate multiple domain servers on one server. For server caches, docker-compose supports installing and connecting redis and redis-state. Anyone can install web services easily using docker and docker-compose. Af you want to use python and php service at same time, this solution can help you better.
- We provide an open source infrastructure integration solution that can easily service Python, Django, PHP, etc. using docker-compose. You can install the commercial-level customizable nginx service and redis at once, and install and manage more services at once. If you are interested, please visit Devspoon-Projects.
- preparing...
-
Support to make configuration files for each service(conf, certbot) : You can use a shell script to generate conf files for https and proxy settings in nginx. Supports a script to restart docker using crontab to complete certbot authentication of the docker container.
-
Efficiently dockerfile configuration for development and service operation : The log folder is interlocked by "volumes" in docker-compose.yml so that user can can be tracked problems even when the docker container is stopped. Webroot, nginx config, etc. are frequently modified during development so these are interlocked by "volumes"
-
Provide reverse proxy function : Multiple web and app services can be provided through one nginx with php or python and services can be provided simultaneously. A shell script is provided to easily create a proxy config file so that it can be integrated with the web UI of other services.
-
Provides easy distributed service operation method : You can use multiple web servers through proxy, and you can use multiple app servers on one web server.
-
Easy service changes using Docker-compose : In docker-compose, various configuration items are defined and commented out. By deleting comments or adjusting your desired settings, you can easily create an environment that suits your purposes.
-
log file collection : Log files for all services are stored in log/ and can be monitored even after container termination.
-
redis and ssl : Information such as configuration files, data, and keys for Redis and SSL are attached as volumes to the redis and ssl folders in docker-compose, so they can be reused when the container is terminated and restarted.
-
Ready-to-run sample apps : Django (
django_sample), FastAPI (fastapi_sample), Flask (flask_sample), and PHP (php_sample) live underwww/so each of the six stacks (gunicorn / uvicorn / uwsgi / daphne / php-7.3 / php-8.4) can be brought up immediately aftergit clone. Samples are domain-agnostic โ bind tolocalhostfirst, swap to your domain when ready. -
Worker privilege drop (
www-data) : gunicorn / uvicorn / uwsgi / php-fpm workers all run aswww-data(uid 33) โ the container boots as root (foruv syncetc.) but workers are dropped to least privilege. Each composecommandrunschown -R www-data:www-data /www/${PROJECT_DIR}before app startup so SQLite/media writes succeed under the dropped UID. uwsgi master usesuid/gid = www-data; gunicorn arbiter stays root and forks workers via setuid. -
Secret separation (
.env-example) : Every stack ships a tracked.env-example(compose/web-service/nginx_*/.env-example), while the actual.envis gitignored. Copy โ fill in Redis password / Flower credentials / etc., never commit the live file.${VAR:?}checks in the compose files fail-fast if a required secret is missing. -
Bot blocker auto-update + supply-chain hardening : Integrates nginx-ultimate-bad-bot-blocker. Container cron refreshes the blocklist every 6 hours. The installer scripts themselves are pinned to a fixed commit SHA and verified with sha256 at build time โ no
masterfloating reference. -
Dynamic gzip compression : All four nginx config directories (gunicorn / uvicorn / uwsgi / php; daphne reuses gunicorn's) enable
gzip onwith level 5,gzip_min_length 1024, andgzip_proxied anyfor JSON/HTML/CSS/JS/XML/SVG payloads. Proxy-passed backend responses are compressed too. Pre-compressed.gzstatic assets are served viagzip_static on.
-
No DB service : This open source does not provide DB as docker to suggest stable operation. It is recommended to install it on a real server and access it using a network, such as port 3306. We hope that this will be done for distributed services as well. We hope that this will be consider for distributed services as well.
-
Development-oriented docker service : This open source is designed for focused on development-oriented rather than perfect docker container distribution and is suitable for startups or new service development teams with frequent initial modifications and tests.
-
Considering on-premise servers : This solution is built for on-premises servers. However, since it is currently being used as a test and commercial service in OCI (Oracle Cloud Infrastructure), it can be used in environments such as AWS and GCP without problems.
์ด์ ๊ฐ์ด๋๋ docs/operations-guide/nginx-hardening/ ์๋์ ์๋ฆฌ์ฆ๋ก ๊ด๋ฆฌ๋ฉ๋๋ค. ์์์ ์ OPS-GUIDE-001 Master Index โ ์ํ ๋ชจ๋ธ, ์ฐ์ ์์ ๋งคํธ๋ฆญ์ค, ๋ถ๊ธฐ๋ณ ๋ก๋๋งต, ์๋ฆฌ์ฆ ์ธ๋ฑ์ค๋ฅผ ํฌํจํฉ๋๋ค. ๊ฐ sub-guide ๋ ๋๋ฉ์ธ ๋จ์๋ก ๋ถ๋ฆฌ๋์ด ๋
๋ฆฝ์ ์ผ๋ก ๊ฐฑ์ ๋ฉ๋๋ค:
| ๋ฒํธ | ๋ฌธ์ | ๋ค๋ฃจ๋ ์์ญ |
|---|---|---|
| OPS-GUIDE-001 | Master Index | ์ํ ๋ชจ๋ธ, ์ฐ์ ์์ ๋งคํธ๋ฆญ์ค, ๋ก๋๋งต, ๊ณตํต ๋กค๋ฐฑ, ์๋ฆฌ์ฆ ์ธ๋ฑ์ค, review/update ์ ์ฑ |
| OPS-GUIDE-002 | TLS / ์ธ์ฆ์ ์ด์ | ์ธ์ฆ์ ๋ง๋ฃ ๋ชจ๋ํฐ๋ง, HSTS preload, Let's Encrypt ๊ณ์ ๋ฐฑ์ |
| OPS-GUIDE-003 | ์ ํ๋ฆฌ์ผ์ด์ ๊ณ์ธต ๋ฐฉ์ด | WAF (ModSecurity + OWASP CRS), fail2ban, CSP ๋จ๊ณ์ ๋์ |
| OPS-GUIDE-004 | ์ปจํ ์ด๋ / ์ด๋ฏธ์ง ๋ณด์ | ๋ฆฌ์์ค ์ ํ, read-only filesystem, ์ด๋ฏธ์ง ์ทจ์ฝ์ ์ค์บ๋, SBOM/์๋ช , egress ํํฐ๋ง, ๋ฐฑ์๋ ๊ฒฉ๋ฆฌ |
| OPS-GUIDE-005 | ์ด์ ๊ฐ์์ฑ / ๋ก๊ทธ / ๋ฉํธ๋ฆญ | ๋ก๊ทธ ํ์ , Observability ์คํ, ์ปค์คํ ์๋ฌ ํ์ด์ง, ๊ฐ์ฌ ๋ก๊ทธ immutability, secrets ๊ด๋ฆฌ, ๋ฐฑ์ /DR |
| OPS-GUIDE-006 | ์ฃ์ง / ๋คํธ์ํฌ | HTTP/3, real_ip, SSL mount ๋ฒ์, Slowloris, CONTINUATION flood, DDoS playbook |
๊ฐ ๋ฌธ์๋ ๊ทผ๊ฑฐ(Why) โ ํ์ฌ ์ํ โ ๊ตฌํ ๋จ๊ณ + ์ค์ ์ค๋ํซ โ ๊ฒ์ฆ ๋ฐฉ๋ฒ โ ๋ชจ๋ํฐ๋ง โ ๋กค๋ฐฑ โ ํํ ๋น ์ง๋ ํจ์ ์ 7๊ฐ ์ ๋ก ๊ตฌ์ฑ๋ฉ๋๋ค. PR ์ ์๋ฆฌ์ฆ ID ๋ฅผ ์ ๋ชฉ์ ๋ช
์ (OPS-GUIDE-003: WAF Phase 2 exclusion ์ถ๊ฐ) ํ์ฌ ๋ถ๊ธฐ review ๊ฐ ์ถ์ ๊ฐ๋ฅํ๊ฒ ํฉ๋๋ค.
-
Make webroot folder
User have to make new folder under www path Example : /www/home_test -
Make a conf file of nginx
-
PHP service (PHP 7.3 / 8.4 dual-version)
The PHP stack ships in two parallel versions selectable per deployment:
- PHP 7.3 (legacy) โ
romeoz/docker-phpfpm:7.3base, Debian multi-version paths (/etc/php/7.3/fpm/...) - PHP 8.4 (current) โ official
php:8.4-fpm-bookwormbase, single-path layout (/usr/local/etc/php{,-fpm.d}/...)
Each version has its own Dockerfile, compose stack, and PHP config folder (php.ini patched for PHP 8.x removals). The nginx config folder is shared because nothing in it depends on the PHP version. Both stacks bind ports 80/443, so they cannot run simultaneously โ pick one per host.
-
PHP service installation [nginx for php] (shared between 7.3 and 8.4)
In config/web-server/nginx/php There are 2 shell scripts (nginx_http_conf.sh, nginx_https_conf.sh) - nginx_http_conf.sh โ sample_nginx_http.conf โ conf.d/<name>_php_ng_http.conf - nginx_https_conf.sh โ sample_nginx_https.conf โ conf.d/<name>_php_https_ng.conf Use "chmod +x xxxx.sh" command, you activate shell script and run. then it make conf file nginx's a conf file will be in conf.d folder. HTTP output always ends with "_http". if your webroot path has sub-level, input type must be following as "\\/www\\/shop\\/shop_kingsShell script required informations like bellow webroot : ex -> shop_kings domain : ex -> xxxx.com portnumber : ex -> 80 appname : ex -> php-app-7.3 (for the PHP 7.3 stack) or php-app-8.4 (for the PHP 8.4 stack) โ must match container_name in compose/web-service/nginx_php-<ver>/docker-compose.yml serviceport : ex -> 9000 (php-fpm listen port; same in both stacks) filename : ex -> xxxx (it's the name for nginx's conf file) -
PHP service installation [php application] (version-specific folder)
PHP 7.3 โ config/app-server/php-7.3 PHP 8.4 โ config/app-server/php-8.4 (php.ini is patched for PHP 8.x removals: track_errors, sql.safe_mode, session.hash_*, [Interbase], [mcrypt] sections removed; error_reporting cleaned of ~E_STRICT; session.use_only_cookies / use_trans_sid / referer_check commented out. Each patch is annotated inline with a "; [PHP 8.4] ..." comment in the file.) Each folder contains 1 shell script (php_conf.sh) that generates the pool config. Use "chmod +x xxxx.sh" command to activate, then run. It writes to pool.d/. -
Run docker-compose.yml (pick one version)
PHP 7.3 โ cd compose/web-service/nginx_php-7.3 PHP 8.4 โ cd compose/web-service/nginx_php-8.4 Execute docker-compose.yml using "docker compose up -d" command. Before first start, copy .env-example to .env and fill in REDIS_PASSWORD (placeholder REDIS_PASSWORD=CHANGE_ME_REDIS_PASSWORD must be replaced). redis is gated by "profiles: redis" in PHP stacks โ start it via: docker compose --profile redis up -d Cannot run both stacks at once: both bind host ports 80/443. To switch versions: "docker compose stop" in the running stack first, then "up -d" in the other.PHP
.env-example์ ๋ณ์ ์ ์ด Python stack ๊ณผ ๋ค๋ฆ ๋๋ค. PHP ์คํ์ ๋จ์ํ๋ ๋ณ์ (LOG_DRIVER,LOG_OPT_MAXF,LOG_OPT_MAXS,REDIS_PASSWORD,ULIMIT_NOFILE_SOFT/HARD) ๋ง ์ฌ์ฉํฉ๋๋ค. Python stack ์PROJECT_DIR / WORKERS / PROJECT_NAME / FLOWER_* / GUNICORN_PORT๋ PHP ์์๋ ์ ์๋์ง ์์ผ๋ฉฐ,PROJECT_DIR๋์ nginx sample conf ์root /www/php_sample๊ฒฝ๋ก๊ฐ ์ง์ ๊ฐ๋ฆฌํค๋ ๋๋ ํฐ๋ฆฌ(www/php_sample/) ๊ฐ ๊ณ ์ webroot ์ ๋๋ค. ์ php ํ๋ก์ ํธ๋ก ๊ต์ฒดํ๋ ค๋ฉดwww/<myphp>/๋ฅผ ๋ง๋ ๋ค nginx conf ์root /www/<myphp>๋ง ์์ ํ๋ฉด ๋ฉ๋๋ค (compose ๋ณ์ ๋ณ๊ฒฝ ๋ถํ์).
- PHP 7.3 (legacy) โ
-
Gunicorn service
-
Gunicorn service installation [nginx for gunicorn]
In config/web-server/gunicorn There are 2 shell script Use "chmod +x xxxx.sh" command, you activate shell script and run.sh then it make conf file nginx's a conf file will be in conf.d folder * if your webroot path has sub-level, input type must be following as "\\/www\\/shop\\/shop_kingsShell script required informations like bellow webroot : ex -> shop_kings domain : ex -> xxxx.com portnumber : ex -> 80 appname : ex -> gunicorn-app (user must be use "container name" referenced in docker-compose.yml file) serviceport : ex -> 8000 (gunicorn application service port) filename : ex -> xxxx (it's the name for nginx's conf file) -
Gunicorn service installation [gunicorn application]
In config/app-server/gunicorn/ - gunicorn.conf.py โ Gunicorn ์ค์ (workers, bind, user="www-data", group="www-data" ๋ฑ) run.sh / make_run.sh ํจํด์ ์ฌ์ฉํ์ง ์์ต๋๋ค. ํ์ฌ docker-compose.yml ์ gunicorn-app service ๊ฐ ์ง์ : command: bash -c "chown -R www-data:www-data /www/${PROJECT_DIR} \ && uv sync --inexact --extra gunicorn --extra celery \ && exec gunicorn -c /gunicorn/gunicorn.conf.py" ์ ํ ์ค๋ก (1) /www/${PROJECT_DIR} ์์ ๊ถ์ www-data ๋ก ๊ฐํ (์์ปค ๊ถํ ๊ฐํ, ยง0.5) (2) uv ๊ฐ pyproject.toml ์ [gunicorn,celery] extras ๋ฅผ ์ปจํ ์ด๋ site-packages ์ ๋๊ธฐํ (venv ๋ฏธ์ฌ์ฉ ์ ์ฑ ยง8) (3) gunicorn ์ ์ฌ์ ์์ฑ๋ /gunicorn/gunicorn.conf.py ๋ก ๊ธฐ๋ ์ ๋ชจ๋ ์ฒ๋ฆฌํฉ๋๋ค. ๋ณ๋ run.sh ์์ฑ ๋ถํ์. ์ ํ๋ก์ ํธ๋ก ๊ต์ฒดํ๋ ค๋ฉด .env ์ PROJECT_DIR ๋ง ๋ฐ๊พธ์ธ์. -
Run docker-compose.yml
Get move to compose/web-service/nginx_gunicorn Before first start, copy .env-example to .env and fill in REDIS_PASSWORD, FLOWER_ID, FLOWER_PWD. CELERY_BROKER_URL is no longer stored in .env โ it is composed from REDIS_PASSWORD at compose time (SSOT, see ยง0.5.3). Run docker-compose.yml using "docker compose up -d". For celery / celery-beat / flower: "docker compose --profile celery up -d". (redis-stats has been removed โ see ยง0.5.7)
-
-
UWSGI service
-
UWSGI service installation [nginx for uwsgi]
In config/web-server/uwsgi There are 2 shell script Use "chmod +x xxxx.sh" command, you activate shell script and run.sh then it make conf file nginx's a conf file will be in conf.d folder * if your webroot path has sub-level, input type must be following as "\\/www\\/shop\\/shop_kingsShell script required informations like bellow webroot : ex -> shop_kings domain : ex -> xxxx.com portnumber : ex -> 80 appname : ex -> uwsgi-app (user must be use "container name" referenced in docker-compose.yml file) serviceport : ex -> 8000 (uwsgi application service port) filename : ex -> xxxx (it's the name for nginx's conf file) -
UWSGI service installation [uwsgi application]
In config/app-server/uwsgi - uwsgi_conf.sh โ uwsgi.ini ์์ฑ๊ธฐ (๋๋ฉ์ธ / chdir / module ๋ฑ ์ ๋ ฅ) - uwsgi.ini โ ์์ฑ๊ธฐ ์ฐ์ถ๋ฌผ (master uid=33 gid=33 ๊ถํ ๊ฐํ ํฌํจ) run.sh / make_run.sh ํจํด์ ์ฌ์ฉํ์ง ์์ต๋๋ค. ํ์ฌ docker-compose.yml ์ uwsgi-app service ๊ฐ ์ง์ : command: bash -c "chown -R www-data:www-data /www/${PROJECT_DIR} \ && uv sync --inexact --extra uwsgi --extra celery \ && exec uwsgi --ini /uwsgi/uwsgi.ini" ์ ํ ์ค๋ก (1) /www ์์ ๊ถ ๊ฐํ (2) uv ์ [uwsgi,celery] extras ๋๊ธฐํ (3) uwsgi master ๊ธฐ๋ ์ ๋ชจ๋ ์ฒ๋ฆฌํฉ๋๋ค. ๋ณ๋ run.sh ์์ฑ ๋ถํ์. ์ ํ๋ก์ ํธ๋ก ๊ต์ฒดํ๋ ค๋ฉด .env ์ PROJECT_DIR ๋ง ๋ฐ๊พธ์ธ์. -
Run docker-compose.yml
Get move to compose/web-service/nginx_uwsgi Before first start, copy .env-example to .env and fill in REDIS_PASSWORD, FLOWER_ID, FLOWER_PWD. CELERY_BROKER_URL is no longer in .env (see ยง0.5.3). Execute docker-compose.yml using "docker compose up -d". For celery / celery-beat / flower: "docker compose --profile celery up -d". (redis-stats has been removed โ see ยง0.5.7)
-
-
Uvicorn (ASGI) service
ยง0.5.9 ๋๊ธฐํ์์
config/web-server/nginx/uvicorn/(์ ์ฒด) ์config/app-server/uvicorn/gunicorn_uvicorn.conf.py๊ฐ ์ ์ค๋์ด ๋ณธ ์คํ์ nginx ๋จ์์ ์ ์ generate / ์ด์ํ ์ ์๊ฒ ๋์์ต๋๋ค. ์ด์ ๋ฒ์ ์๋compose/.../nginx_uvicorn/์คํ๋ง ์กด์ฌํ๊ณ nginx conf ํด๋๊ฐ ์์ด, ๋๋ฉ์ธ conf ๋ฅผ ๋ง๋ค ๋ฐฉ๋ฒ์ด ์์์ต๋๋ค.-
Uvicorn service installation [nginx for uvicorn]
In config/web-server/nginx/uvicorn There are 2 shell scripts (nginx_http_conf.sh, nginx_https_conf.sh) - nginx_http_conf.sh โ sample_nginx_http.conf โ conf.d/<name>_uvicorn_ng_http.conf - nginx_https_conf.sh โ sample_nginx_https.conf โ conf.d/<name>_uvicorn_ng_https.conf Use "chmod +x xxxx.sh" command, you activate shell script and run.Shell script required informations like bellow webroot : ex -> fastapi_sample (or django_sample for ASGI Django) domain : ex -> xxxx.com portnumber : ex -> 80 appname : ex -> uvicorn-app โ must match container_name in compose/web-service/nginx_uvicorn/docker-compose.yml serviceport : ex -> 8000 filename : ex -> xxxx (it's the name for nginx's conf file) -
Uvicorn service installation [uvicorn application]
In config/app-server/uvicorn there are TWO config files: - uvicorn.conf.py โ ํ์ค uvicorn ์ง์ ๊ธฐ๋ ์ ์ฌ์ฉ - gunicorn_uvicorn.conf.py โ gunicorn + UvicornWorker ํจํด (๋ค์ ์์ปค prefork ASGI) Both load via UV_PROJECT_ENVIRONMENT=/usr/local (venv ๋ฏธ์ฌ์ฉ ์ ์ฑ , ยง8). -
Run docker-compose.yml
Get move to compose/web-service/nginx_uvicorn Before first start, copy .env-example to .env and fill in REDIS_PASSWORD, FLOWER_ID, FLOWER_PWD. CELERY_BROKER_URL is composed from REDIS_PASSWORD (ยง0.5.3). Execute docker-compose.yml using "docker compose up -d". For celery / celery-beat / flower: "docker compose --profile celery up -d".
-
-
Daphne (ASGI WebSocket) service โ devspoon ๊ณ ์ ์คํ (aisum-infrakit ์๋ ์์)
Stack: compose/web-service/nginx_daphne/ Logrotate dropins: script/logrotate/daphne/{daphne,celery/daphne-celery,celerybeat/daphne-celerybeat} Image: shares devspoon-py-app:latest (gunicorn / uvicorn ๊ณผ ๋์ผ ๋ฒ ์ด์ค, ยง0.5.4) Use case: Django Channels ๊ฐ์ WebSocket-only ์๊ตฌ์ฌํญ. nginx conf ๋ gunicorn ์คํ์ sample ์ base ๋ก location ๋ณ ws_pass ๋ฅผ ์ถ๊ฐํ์ฌ ์ฌ์ฉ.
-
www/ ์๋์ ์ํ ์ฑ์ ๊ฐ ์คํ์ ์ฆ์ ๊ฒ์ฆํ๊ธฐ ์ํ reference ์
๋๋ค. ์ด์ ์ฝ๋๋ ๊ฐ์ ์์น์ ์์ ์ ํด๋๋ก ๋๋ฉด ๋ฉ๋๋ค.
| ํด๋ | ๋ฐฑ์๋ / ์คํ | ๋น๊ณ |
|---|---|---|
www/django_sample/ |
gunicorn / uwsgi / daphne / uvicorn ๋ชจ๋์์ ์ฌ์ฉ ๊ฐ๋ฅ. Django 4.0.6 + uv-managed (pyproject.toml, uv.lock) |
.python-version = 3.14. ํธ์คํธ์์ uv sync ๊ฐ .venv ์๋ ์์ฑ, ์ปจํ
์ด๋์์ ์์คํ
site-packages ์ง์ค์น (ยง8) |
www/fastapi_sample/ |
uvicorn ์ ์ฉ. FastAPI ์ต์ , uv-managed | ยง0.5.9 ๋์ โ uvicorn ์คํ์ ๋์ ๊ฒ์ฆ์ฉ |
www/flask_sample/ |
gunicorn ๋๋ uwsgi ์ ์ฉ (WSGI). Flask, uv-managed | ยง0.5.9 ๋์ โ WSGI ์คํ์ ๋น-Django ๊ฒ์ฆ์ฉ |
www/php_sample/ |
php-fpm (7.3 ๋๋ 8.4) ์ฉ. ๋จ์ผ index.php |
์ปจํ
์ด๋ ๋ด๋ถ ๋ง์ดํธ /var/www/html |
www/certbot/ |
์ปจํ ์ด๋ ์ ACME webroot ํ์ค ์์น | .gitkeep ๋ง ๋์ด ๋น ๋๋ ํ ๋ฆฌ ์ถ์ . ๋๋ฉ์ธ ๋ฐ๊ธ ์ nginx conf ์ /.well-known/acme-challenge/ ๊ฐ ์ด ๊ฒฝ๋ก๋ก alias |
๊ธฐ์กด ์ฌ์ฉ ํ๋ฆ์ ๊ทธ๋๋ก:
1) ์ ์ฑ: www/myapp/ ๋ฅผ ๋ง๋ ๋ค.
2) config/web-server/nginx/<stack>/nginx_http_conf.sh -w myapp -d ... ๋ก ๋๋ฉ์ธ conf ์์ฑ.
3) compose/web-service/nginx_<stack>/.env ์ PROJECT_DIR=myapp ์ผ๋ก ์ค์ .
(docker-compose.yml ์ ${PROJECT_DIR} ๋ณ์ ์นํ๋ง ํ๋ฉฐ, ๊ฐ ์์ฒด๋ .env ๊ฐ ๋ณด์ )
4) docker compose up -d --build
๊ฐ ์คํ์ config/web-server/nginx/<stack>/conf.d/ ๋๋ ํฐ๋ฆฌ์๋ 2 ์ข
๋ฅ์ ์๊ตฌ .conf ๋ง ์กด์ฌํฉ๋๋ค (.example ํจํด ๊ธ์ง โ ๋จ์ผ ํ์ฅ์ ์ ์ฑ
):
| ํ์ผ | ์ญํ | ๋์ |
|---|---|---|
default.conf |
catch-all ๋จ๋ ์ฑ ์ | listen 80 default_server + listen 443 ssl default_server + server_name _ + return 444 / ssl_reject_handshake on. ์ ์ ์๋ Host/SNI ์ฐจ๋จ. default_server ๊ฐ ์ ์๋๋ ์ ์ผํ ์์น |
<sample>_<stack>_ng_http.conf |
์ฆ์ ๊ฒ์ฆ์ฉ ์๊ตฌ sample | listen 80; (default_server ์์) + server_name localhost www.localhost; ๋ก ํ์ . nginx ์ปจํ
์ด๋ ์์ ์ ์๋ ๋ก๋๋์ด Host: localhost ๋ก HTTP 200 ์๋ต ๊ฐ๋ฅ |
| Stack | sample conf ํ์ผ |
|---|---|
| gunicorn | django_sample_gunicorn_ng_http.conf |
| uvicorn | django_sample_uvicorn_ng_http.conf |
| uwsgi | django_sample_uwsgi_ng_http.conf |
| php (7.3/8.4 ๊ณต์ฉ) | sample_php_ng_http.conf |
์ฆ์ ๊ฒ์ฆ (compose up ์งํ):
curl -H "Host: localhost" http://localhost/ # โ 200๋ณ๋ ํ์ฑํ ๋จ๊ณ (์: cp .example .conf) ๋ถํ์. sample ์ listen 80; ์ default.conf ์ listen 80 default_server; ๊ฐ ๋์ผ ํฌํธ๋ฅผ ๊ณต์ ํด๋ default_server ํค์๋๊ฐ default.conf ์๋ง ์์ด ์ถฉ๋ ์์.
๋๋ฉ์ธ๋ณ ์ด์ conf ์ถ๊ฐ: nginx_http_conf.sh -w <webroot> -d <domain> -p 80 -a <appname> -s <serviceport> -n <name> ๋ก conf.d/<name>_<stack>_ng_http.conf ๋ฅผ ์์ฑ โ sample ๊ณผ ๋ค๋ฅธ server_name ์ ๊ฐ์ง๋ฏ๋ก ๊ณต์กดํฉ๋๋ค. ์ด์ ์ sample ์ ๋นํ์ฑํํ๋ ค๋ฉด ํด๋น ํ์ผ์ conf.d/ ๋ฐ์ผ๋ก ์ฎ๊ธฐ๊ฑฐ๋ ์ญ์ ํ์ธ์ (์๊ตฌ .conf ์ด๋ฏ๋ก git ์ถ์ ).
-
User can access using defined folders in docker-compose.yml
Example -> nginx container has volumes like below that /www /script/ /etc/nginx/conf.d/ /etc/nginx/nginx.conf /etc/nginx/uwsgi_params /ssl/ /log- If user run containers at same server, can update code and move files directly from local server folder to container folder.
-
If user use firewall, have to add required port number (refer each docker-compose.yml files)
Example ufw allow 80/tcp ufw allow 3306/tcp
-
This step requires running http nginx server
-
Run nginx_http_conf.sh located in config/web-server/nginx/. Create a conf file for each domain under config/web-server//conf.d/. Generated filenames always end with "_http" (e.g. _gunicorn_ng_http.conf).
-
Please edit compose/web-service//docker-compose directly and run it according to the service you want to use.
-
This will run the default nginx using http.
-
The "docker exec -it bash" command allows users to access docker internals.
-
The script/letsencrypt.sh shell script file is linked per volume. This allows users to access script files directly from the nginx container.
-
Run script/letsencrypt.sh and enter information such as web root, domain, and email. This script automatically creates an SSL key for your volume if it does not exist.
-
If you entered all keys correctly, use the exit command to exit the container.
-
Now we need to create a conf file for https and delete the existing file.
-
Run nginx_https_conf.sh located in config/web-server/nginx/. Create a conf file for each domain under config/web-server//conf.d/.
-
Users must remove the http conf file from config/web-server//conf.d/.
-
Run the โdocker-compose restartโ command in the compose folder. You can also use the โdocker-compose stopโ and โdocker-compose startโ commands in the compose folder. Do not use the "docker-compose down" command. Related configuration files may be deleted.
-
Certbot ๊ฐฑ์ cron ์ ์ปจํ ์ด๋ ์์ ๋ด์ฅ๋์ด ์์ต๋๋ค (
docker/<stack>/entrypoint-with-cron.sh๊ฐ ๋ถํ ์ ๋ฑ๋ก). ํธ์คํธ์์ ๋ณ๋crontab์ค์ / ์ธ๋ถ ์คํฌ๋ฆฝํธ ์คํ์ ๋ถํ์ ํฉ๋๋ค. ๊ฐฑ์ ์--deploy-hook "nginx -t && nginx -s reload"๋ก ์ธ์ฆ์๊ฐ ๋ฐ๋ ๊ฒฝ์ฐ์๋ง nginx ๊ฐ graceful reload ํฉ๋๋ค. ์์ธํ cron ์ง์ค ์์ค๋ ยง3 "๊ฐฑ์ cron ์ ์ง์ค ์์ค" ์ฐธ์กฐ. -
์ปจํ ์ด๋ ๋ด๋ถ์์ cron ๋ฑ๋ก ํ์ธ:
docker compose exec webserver crontab -l๋๋docker compose exec gunicorn-app crontab -l(๊ฐ stack ์ entrypoint ๊ฐ ๋ฑ๋ก).
-
๋ณธ ์น์ ์ ์ธํ๋ผ/์ด์์ ๊ด์ ์์ ๋ณธ ํ๋ก์ ํธ๋ฅผ ์์ ํ๊ฒ ๋ฐฐํฌยท์ด์ํ๊ธฐ ์ํ ์ฌ์ ์ง์, ๊ถ์ฅ ์ค์ , ๊ทธ๋ฆฌ๊ณ ์ฃผ์ํด์ผ ํ ๋์์ ์ ๋ฆฌํ ๋ฌธ์์ ๋๋ค. ํ๋ก๋์ ๋ฐฐํฌ ์ ์ ๋ฐ๋์ ์ฝ์ด์ฃผ์ธ์.
| ํญ๋ชฉ | ๊ฐ | ๋น๊ณ |
|---|---|---|
| ์๋ฒ ์ฌ์ | 8 core / 8 GB RAM | ๋ณธ README ์ ๋ชจ๋ ํ๋ ์์น๋ ์ด ๊ธฐ์ค์์ ์ฐ์ ๋จ. ์ฌ์์ด ๋ค๋ฅด๋ฉด ์์ปค/ํ ์์น ์ฌ์ฐ์ ํ์ |
| ๋ฐฐํฌ ๋ฐฉ์ | ์๋ stop/start (๋ฌด์ค๋จ ๋ฏธ๊ณ ๋ ค) | docker compose stop โ ์ฝ๋ ๊ฐฑ์ โ docker compose start. zero-downtime / blue-green / rolling ๋ฏธ์ง์ |
| ์ปจํ ์ด๋ ๋ฒ ์ด์ค | ubuntu:24.04 (LTS), nginx:1.27-bookworm |
latest ํ๊ทธ ๊ธ์ง โ ์ฌํ ๊ฐ๋ฅ ๋น๋ ๋ณด์ฅ |
| Python | 3.14.0 (์์ค ์ปดํ์ผ, multi-stage builder + SHA256 ๊ฒ์ฆ) |
๋น๋ ์๊ฐ ๊ธธ์ง๋ง ๋ฐํ์ 5-10% ๋น ๋ฆ. SHA256 ARG default = 2299dae5...e9f3e9 (ยง0.5.4) |
| PHP 7.3 (legacy) | romeoz/docker-phpfpm:7.3 |
docker/php-fpm/Dockerfile-7.3. ๋ฉํฐ๋ฒ์ ๊ฒฝ๋ก(/etc/php/7.3/fpm/...). ์
์คํธ๋ฆผ ๋น์ ์ง๋ณด์ โ ์ ๊ท ๋ฐฐํฌ๋ 8.4 ๊ถ์ฅ |
| PHP 8.4 (current) | php:8.4-fpm-bookworm (๊ณต์) |
docker/php-fpm/Dockerfile-8.4. ๋จ์ผ ๊ฒฝ๋ก(/usr/local/etc/php{,-fpm.d}/...). php.ini ๋ 8.x ํธํ์ผ๋ก ํจ์น๋จ (config/app-server/php-8.4/php_ini/php.ini) |
| Redis | redis:7.4-alpine + protected-mode yes + requirepass |
Alpine ๋ฒ ์ด์ค๋ก ์ด๋ฏธ์ง 100 MB ์ดํ. ์ธ์ฆ ๊ฐ์ (ยง0.5.2) |
| Flower | mher/flower:2.0.1 |
master ํ๊ทธ๋ ์ฌํ์ฑ ์์ผ๋ฏ๋ก ๊ธ์ง. FLOWER_BASIC_AUTH ํ์ |
| ์ด๋ฏธ์ง ํ๊ทธ (์ด ํ๋ก์ ํธ๊ฐ ๋น๋) | devspoon-py-app:latest, devspoon-uwsgi-app:latest, devspoon-nginx:latest, devspoon-php-app:{7.3,8.4} |
compose image: ๋ช
์๋ก ์คํ / ์๋น์ค ๊ฐ ์ฌ์ฌ์ฉ (ยง0.5.4) |
- ๋ณธ ํ๋ก์ ํธ๋ ๋จ์ผ ์๋ฒ(8c/8g) ์ด์ฉ์ 1์ฐจ ํ๊น์ผ๋ก ํ๋ฉฐ, ๋ก๋๋ฐธ๋ฐ์/์ค์ผ์คํธ๋ ์ดํฐ(K8s)๊ฐ ์๋ ํ๊ฒฝ์์ ๊ฐ์ฅ ๋จ์ยท์์ ํ ๋ฐฐํฌ ๋ชจ๋ธ.
- ๋ฌด์ค๋จ(graceful HUP reload) ์ ํฌ๊ธฐํ ๋์ ๋ฉ๋ชจ๋ฆฌ ์ ๊ฐ ์ ์ฐ์ :
- gunicorn
preload_app=True(Copy-on-Write๋ก ์์ปค ๋ฉ๋ชจ๋ฆฌ 20-40% ๊ฐ์) - uwsgi
lazy-apps=false(๋ง์คํฐ์์ 1ํ ๋ก๋ ํ fork)
- gunicorn
- ๋ฌด์ค๋จ์ด ํ์ํด์ง๋ ์์ ์ ๋ณ๋ PR/๋ง์ด๊ทธ๋ ์ด์ ์ผ๋ก ์ฒ๋ฆฌํ๋ ๊ฒ์ ๊ถ์ฅ.
๋ณธ ์ ์ ๊ฐ์ฅ ์ต๊ทผ์ ์ผ๊ด ์ ์ฉ๋ ์ ์ฑ ๋ณ๊ฒฝ์ ๋ชจ์ ๋๋ค โ ์ด์ README ์ ๊ธฐ๋ณธ๊ฐ๊ณผ ๋ค๋ฅธ ๋ถ๋ถ์ด ์์ผ๋ ์ด์์๋ ๋ฐ๋์ ๋ณธ ์ ์ ํ์ธ ํ ยง1 ์ดํ ์ด์ ๊ฐ์ด๋๋ฅผ ์ฝ์ ๊ฒ.
- 6๊ฐ ์คํ(daphne / gunicorn / uvicorn / uwsgi / php-7.3 / php-8.4) ๊ฐ๊ฐ์
compose/web-service/<stack>/.env๋ git ์ถ์ ๋์์์ ์ ์ธ. ๋์ผ ํด๋์.env-example๋ง ์ถ์ ๋๋ฉฐ, ์ ๊ท ํ๊ฒฝ์cp .env-example .envํ ์๊ฒฉ์ฆ๋ช ์ ์ฑ์ ์์. .gitignore์ ํจํด:**/.env(ignore) +!**/.env-example(์์ธ ์ถ์ ). ๊ธฐ์กด์ ์ถ์ ๋๋ 5๊ฐ.env๋git rm --cached๋ก untrack ๋จ (์์ ํธ๋ฆฌ ๋ณด์กด).- compose ์
${VAR:?error}๊ฒ์ฆ: ์๊ฒฉ์ฆ๋ช ํค(REDIS_PASSWORD/FLOWER_ID/FLOWER_PWD) ๊ฐ ๋ฏธ์ค์ /๋น๋ฌธ์์ด์ด๋ฉดdocker compose up๋จ๊ณ์์ ์ฆ์ fail-fast โ "๋น๋ฐ๋ฒํธ ๋น๊ฐ ๊ธฐ๋" ์ฌ๊ณ ์ฐจ๋จ. - ๋ก๊ทธ ์ต์
ํค(
LOG_DRIVER/LOG_OPT_MAXF/LOG_OPT_MAXS) ๋${VAR:-default}๋ก fallback ์ฒ๋ฆฌ๋์ด.env๋๋ฝ์๋ ๋ฌด์ํฅ.
- 6๊ฐ
redis.conf๋ชจ๋protected-mode no โ yes. ๋์ผ docker network ๋ด๋ถ์์๋ ์ธ์ฆ ์์ด๋ ์ด๋ค ํค์๋ ์ ๊ทผ ๋ถ๊ฐ. requirepass๊ฐ์redis.conf๊ฐ ํ๊ฒฝ๋ณ์ ๋ณด๊ฐ์ ์ง์ํ์ง ์์ผ๋ฏ๋ก compose ์command: redis-server ... --requirepass ${REDIS_PASSWORD}๋ก ์ธ๋ถ ์ฃผ์ .- redis ์ปจํ
์ด๋์ healthcheck ์ถ๊ฐ:
healthcheck: test: ["CMD-SHELL", "redis-cli --no-auth-warning -a \"$$REDIS_PASSWORD\" ping | grep -q PONG"] interval: 10s timeout: 3s retries: 5 start_period: 10s
- ์ฑ / celery / celery-beat / flower ์
depends_on: redis๊ฐcondition: service_healthy๋ก ๊ฐํ โ redis ๊ฐ ์ธ์ฆ ์๋ต์ ์ค ๋๊น์ง ์์กด ์๋น์ค ๊ธฐ๋์ ๋ณด๋ฅํด race condition ์ฐจ๋จ.
- ๊ณผ๊ฑฐ
.env์CELERY_BROKER_URL=redis://:PASS@redis:6379/3ํํ๋ก ๋ณ๋ ๋ณด๊ด โREDIS_PASSWORD์ ๋ ๊ฐ์ ๋๊ธฐํํด์ผ ํ๋ drift ์ํ ์กด์ฌ. - ์ด๋ฒ ๋ณ๊ฒฝ์ผ๋ก
.env์CELERY_BROKER_URLํค ์ ๊ฑฐ. compose ์ celery / celery-beat / flowerenvironment:์์redis://:${REDIS_PASSWORD:?...}@redis:6379/3๋ก ์ง์ ํฉ์ฑ โ REDIS_PASSWORD ํ ๊ฐ๋ง ๊ฐฑ์ ํ๋ฉด 4๊ฐ ์๋น์ค๊ฐ ๋์์ ๋๊ธฐํ. - ์ถ๊ฐ ํจ๊ณผ: ๊ณผ๊ฑฐ์๋ celery / celery-beat ์ปจํ
์ด๋์
CELERY_BROKER_URLํ๊ฒฝ๋ณ์๊ฐ ์ฃผ์ ๋์ง ์์ Django settings ๊ฐos.environ['CELERY_BROKER_URL']์ ์ฝ์ผ๋ฉด ๊ธฐ๋ณธ๊ฐamqp://localhost๋ก ๋จ์ด์ง๋ ์ ์ฌ ๋ฒ๊ทธ ์กด์ฌ โ ๋ณธ ๋ณ๊ฒฝ์ผ๋ก ํด์.
docker/gunicorn/Dockerfile์docker/uwsgi/Dockerfile์ multi-stage ๋ก ์ฌ๊ตฌ์ฑ:- builder stage:
ubuntu:24.04+build-essential+*-devlibs + Python 3.14 ์์ค ์ปดํ์ผ + pip ์ฌ์ ์ค์น (native build ํ์ํ mysqlclient/psycopg2 ๋ฑ ํฌํจ). - runtime stage:
ubuntu:24.04+ ๋ฐํ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ง + percona-xtrabackup-84 + pgbackrest + cron + logrotate.build-essential/pkg-config์ ๊ฑฐ๋์ด ์ด๋ฏธ์ง ์ฝ 500MB ์ ๊ฐ.
- builder stage:
- Python tarball SHA256 ๊ฒ์ฆ ์ถ๊ฐ:
ARG PYTHON_SHA256=2299dae542d395ce3883aca00d3c910307cd68e0b2f7336098c8e7b7eee9f3e9(Python 3.14.0 ๊ณต์, 2026-05-18 ํ์ธ).- ๋น๋ ๋จ๊ณ์์
sha256sum -c๊ฐ ๋น0 ์ข ๋ฃํ๋ฉด RUN ์ค๋จ โ ๊ณต๊ธ๋ง ๋ณ์กฐ ๊ฐ์ง. - ๋ฒ์ bump ์ python.org Files ํ ๋๋
.sigstorebundle ๊ฒ์ฆ ํ ARG ๊ฐ ๊ฐฑ์ .
- compose ์
image:๋ช ์ ํ๊ทธ๋ก stack ๊ฐ / ์๋น์ค ๊ฐ ์ฌ์ฌ์ฉ:devspoon-py-app:latestโ gunicorn / daphne / uvicorn 3๊ฐ ์คํ + ๊ฐ ์คํ ๋ด celery / celery-beat ๊ฐ ๊ณต์ .devspoon-uwsgi-app:latestโ uwsgi ์คํ ์ ์ฉ.devspoon-nginx:latest,devspoon-php-app:7.3/devspoon-php-app:8.4.- ํจ๊ณผ:
docker compose build๊ฐ ๋์ผ ์ปจํ ์คํธ๋ฅผ 3-4ํ ์ฌ๋น๋ํ๋ ๋์์ด 1ํ๋ก ์ถ์.
username = root์ ๊ฑฐ โuid = www-data/gid = www-dataํ์ฑ. master ๊ฐ root ๊ฐ ์๋ www-data ๋ก ๋จ์ด์ง๋ฉฐ PHP-FPM ํจํด๊ณผ ๋์นญ (least-privilege).chmod-socket = 666์ฃผ์ํ โ TCP 8000 ์ฌ์ฉ ์ ๋ฌด์๋ฏธ. ํฅํ unix socket ์ ํ ์ 0660 ์ฌ์ฉ ์๋ด๊ฐ ์ธ๋ผ์ธ ์ฝ๋ฉํธ๋ก ์ถ๊ฐ๋จ.sample_uwsgi.ini์py-autoreload = 1 โ 0โ cookiecut ์ ์ด์ ๋ถ์ ํฉ ๊ธฐ๋ณธ๊ฐ์ด ๋ค์ ๋์ง์ง ์๊ฒ ํต์ผ.
.env์ULIMIT_NOFILE_SOFT=65535/ULIMIT_NOFILE_HARD=65535๋ณ์ ์ ์.- 6๊ฐ compose ์
webserver์๋น์ค์ ulimits ๋ธ๋ก์ ์ฃผ์ ์ฒ๋ฆฌ ์ํ๋ก ํฌํจ โ ํธ์คํธ docker daemon ์ ๊ธฐ๋ณธ LimitNOFILE (ํต์ 1048576) ์ด 65535 ๋ฅผ ์ถฉ๋ถํ ์์ฉํ๊ธฐ ๋๋ฌธ. ํธ์คํธ OS ์ฐจ์ด๋ก nofile ํ๋๊ฐ 65535 ๋ฏธ๋ง์ผ๋ก ์๋ฆฌ๋ ํ๊ฒฝ(RHEL / podman / ์ผ๋ถ K8s ๋ ธ๋)์์๋ง ์ฃผ์ ํด์ ํ์ฌ ๋ช ์ ํ์ฑํ.
insready/redis-stat:latest(2017๋ ์ดํ ๋ฏธ๊ด๋ฆฌ, ๋ณด์ ํจ์น ์์) ๊ฐ ๋ชจ๋ 6๊ฐ compose ์์ ์ญ์ ๋จ. ํธ์คํธ63790/tcp๋ ธ์ถ๋ ํจ๊ป ์ ๊ฑฐ.- ๋์ฒด๊ฐ ํ์ํ๋ฉด RedisInsight ๋๋
oliver006/redis_exporter(Prometheus์ฉ) ๋์ ๊ถ์ฅ.
Dockerfile-7.3/Dockerfile-8.4์์dpkg-reconfigure tzdata์ ๊ฑฐ. gunicorn / uwsgi / nginx Dockerfile ๊ณผ ๋์ผํ๊ฒln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezoneํจํด์ผ๋ก ํต์ผ.Dockerfile-7.3์ ์ค๋ณต install layer 6๊ฐ๋ฅผ ๋จ์ผ RUN ์ผ๋ก ์์ถ.
aisum-infrakit (๋ณธ ํ๋ก์ ํธ์ ์ฌ๋ด ํ์๋ณธ) ์ ์ด์ ๊ฒ์ฆ ์ฐ์ถ๋ฌผ์ ์ญ๋จธ์งํ์ฌ ๋ค์ ํญ๋ชฉ์ด ์ผ๊ด ์ ํฉํ๋์์ต๋๋ค:
| ํญ๋ชฉ | ๋ณ๊ฒฝ | ์๋ |
|---|---|---|
ํด๋๋ช
compose/web_service/ โ compose/web-service/ |
dash naming | aisum-infrakit ์ ๋์ผ โ ์ธ๋ถ ๋ฌธ์/์คํฌ๋ฆฝํธ ํธํ |
ํ์ผ๋ช
.env.example โ .env-example (6๊ฐ ์คํ) |
dash naming | aisum-infrakit ์ ๋์ผ |
config/web-server/nginx/uvicorn/ ์ ์ค |
๋๋ ํ ๋ฆฌ ์ถ๊ฐ | ๊ธฐ์กด์ nginx_uvicorn ์ปดํฌ์ฆ ์คํ๋ง ์๊ณ nginx conf ๊ฐ ๋๋ฝ โ uvicorn ๋๋ฉ์ธ conf ๋ฅผ ์์ฑํ ๋ฐฉ๋ฒ์ด ์์๋ค |
config/app-server/uvicorn/gunicorn_uvicorn.conf.py ์ ์ค |
gunicorn+UvicornWorker ์ค์ | ASGI ์์์ gunicorn ์ผ๋ก ์์ปค๋ฅผ ๋์ฐ๋ ํจํด ์ง์ |
| dhparam ๋ฐฑ์ /๋ณต์ ๋ฉ์ปค๋์ฆ | ssl/certs:/etc/ssl/certs ์ํฐํจํด ์ ๊ฑฐ, ssl/dhparam:/etc/nginx/dhparam-backup ์ผ๋ก ๊ต์ฒด + nginx Dockerfile ์ ๋น๋ ์ ๊ตฝ๊ธฐ + entrypoint hook (ยง3 dhparam ์์ํ ์ ) |
certbot ์์คํ CA ๊ฐ๋ฆผ ์ ๊ฑฐ, docker compose down/up ํ์๋ ๋์ผ ํค ์ ์ง |
script/test_run/ ์ ์ค (21 ๊ฐ ์คํฌ๋ฆฝํธ) |
aisum-infrakit ์ ํ๊ท ๊ฒ์ฆ ์์ฐ ๋์ โ s0_prereq, s1b_exit_check, s2_build, s3_stack_smoke, s5_https, s6_regression, ssl_diag, verify_block ๋ฑ + audit ๋ผ์ด๋์์ ์ถ๊ฐ๋ verify_dhparam_lifecycle / verify_dhparam_host_wins / verify_conf_generators / verify_compose_yml / verify_nginx_standalone | ๊ฒ์ฆ ์๋ํ (ยง10.3 ์ฐธ์กฐ). ๊ธฐ์กด script/test/preflight.sh / verify-ngxblocker.sh ์ ๊ณต์กด |
script/logrotate/* ์ su root root ์ถ๊ฐ |
logrotate ์ "potentially insecure mode" ๊ฑฐ๋ถ ํํผ (WSL /mnt/c 0777 mount ํ๊ฒฝ) |
bind mount ํ๊ฒฝ์์ ๋กํ ์ด์ ์คํจ ํด์ |
docker/gunicorn/Dockerfile / docker/uwsgi/Dockerfile ์์กด์ฑ ๋ณด๊ฐ |
builder: libbz2-dev liblzma-dev ์ถ๊ฐ / runtime: libbz2-1.0 liblzma5 libnsl2 libuuid1 ์ถ๊ฐ (uwsgi ๋ libpcre3* libxml2* ์ถ๊ฐ) |
Python stdlib (bz2 / lzma) ์ uwsgi ๋ผ์ฐํ
/ํ๋ฌ๊ทธ์ธ dlopen ์คํจ ๋ฐฉ์ง |
entrypoint-with-cron.sh 3์ข
sanitize ๋ก์ง ์ถ๊ฐ |
bind-mount ๋ logrotate dropin ์ /run/logrotate.d/ ๋ก mode 0644 ์ฌ๋ณธํ |
WSL 0777 mount ์์ logrotate "potentially dangerous mode" ๊ฑฐ๋ถ ํํผ |
www/fastapi_sample/ / www/flask_sample/ / www/certbot/ ์ ์ค |
aisum-infrakit ์ ์ํ ์ฑ ๋์ | uvicorn (FastAPI) / ์ผ๋ฐ WSGI (Flask) ์คํ์ ๋์ ํ์ธ์ฉ. www/certbot/ ์ ACME webroot ํ์ค ์์น |
script/test_run/* ์ ํ๋์ฝ๋ฉ ROOT ๊ฒฝ๋ก |
/mnt/c/.../aisum-infrakit โ /mnt/c/.../devspoon-web ๋ก ์ผ๊ด ์นํ๋จ |
๋์ผ ์คํฌ๋ฆฝํธ๊ฐ devspoon-web ์์ ์ฆ์ ๋์ |
๋ณด์กด (๋ฎ์ด์ฐ์ง ์์):
nginx_daphne์คํ (devspoon ๊ณ ์ ),script/logrotate/daphne/*,docs/operations-guide/nginx-hardening/*docker/php-fpm/Dockerfile-7.3/Dockerfile-8.4์ PHP ๋ฒ์ ๋ถ๋ฆฌ (aisum ์ ๋จ์ผ PHP 7.2 ๋ง)docker/{gunicorn,uwsgi,php-fpm}/entrypoint-with-cron.sh๊ตฌ์กฐ (aisum ์ Dockerfile ์ธ๋ผ์ธ ํจํด)redis.conf์protected-mode yes(aisum ์no. devspoon ์ ์ฑ ์ฐ์)CELERY_BROKER_URL์ compose ํฉ์ฑ(SSOT) (aisum ์ .env ์ ๋ณ๋ ๋ณด๊ด)script/letsencrypt.sh(devspoon ๋ฒ์ ์ด ๋ ์งํ)nginx_php-7.3/8.4์ 2๊ฐ ๋ณํ ์คํ +config/app-server/php-7.3/php-8.4๋ถ๋ฆฌ
8 core / 8 GB ๊ธฐ์ค ๋ฉ๋ชจ๋ฆฌ ๊ฐ์ฉ๋ ๊ณ์ฐ: 8 GB - (OS + nginx + redis + ํค๋๋ฃธ โ 2 GB) = ์ฝ 6 GB.
| ์๋น์ค | ํต์ฌ ์์น | ์ฐ์ ๊ทผ๊ฑฐ |
|---|---|---|
| gunicorn (Django sync) | workers=9, threads=4 |
(cores + 1) ๋ณด์์น. ์์ปค๋น ~250 MB ร 9 โ 2.25 GB. threads=4 ๋ก DB I/O ๋๊ธฐ ํก์ โ ๋์ ์ฌ๋กฏ 36 |
| uvicorn (FastAPI ASGI) | workers=8 (UvicornWorker) |
๋น๋๊ธฐ ์์ปค๋ ๋จ์ผ ์ด๋ฒคํธ ๋ฃจํ๋ก ๋ค์ ๋์ ์ฒ๋ฆฌ. 1 worker / core. ์์ปค๋น ~600 MB ร 8 โ 4.8 GB |
| uwsgi (Django) | processes=8, threads=4 |
sync ์์ปค. harakiri=60, reload-on-rss=800MB (๋ฉ๋ชจ๋ฆฌ ๋์ ์๋ ๋ณต๊ตฌ) |
| daphne (ASGI WS) | ๋จ์ผ ํ๋ก์ธ์ค | daphne ๋ ๋ฉํฐํ๋ก์ธ์ค ๋ฏธ์ง์. ๋์ websocket ์์ฒ์ ๋จ์ผ ์ธ์คํด์ค๋ก ๊ฐ๋ฅ. ๋ ํ์ ์ nginx upstream + ๋ค์ค ์ปจํ ์ด๋ |
| celery worker | --concurrency=8 --max-tasks-per-child=2000 |
prefork ํ, ์ฝ์ด ๋งค์นญ. ๋ฉ๋ชจ๋ฆฌ ๋์ ๋ฐฉ์ด ์ํด 2000 ํ์คํฌ๋ง๋ค ์์ปค ์ฌ๊ธฐ๋ |
| php-fpm (7.3/8.4 ๊ณต์ฉ) | pm.max_children=40, start=8, min_spare=8, max_spare=24 |
์์ปค๋น ~80 MB ร 40 โ 3.2 GB. request_terminate_timeout=60s ๋ก hang ๋ฐฉ์ง. ๋ ๋ฒ์ ๋ชจ๋ ๋์ผ pool ์ ์ฑ
์ ์ ์ฉ โ ๊ฐ ๋ฒ์ ํด๋(config/app-server/php-7.3 / php-8.4)์ pool.d/sample_php.conf ๋ด์ฉ์ ๋์ผํ๋ฉฐ, ์ฐจ์ด๋ php.ini ์ 8.x ํธํ ํจ์น๋ฟ |
์ฃผ์ โ
preload_app=True์ ๋ถ์ ํจ๊ณผ:
- DB ์ปค๋ฅ์ ์ ๋ชจ๋ import ์์ ์ ์ด๋ฉด fork ํ ์์ปค๋ค์ด ๋์ผ ์์ผ์ ๊ณต์ โ ์ถฉ๋ ๊ฐ๋ฅ.
- ํด๊ฒฐ: DB ์ปค๋ฅ์ ์ ์ฒซ ์์ฒญ ์ lazy ์์ฑํ๊ฑฐ๋,
post_forkํ ์์ ๋ช ์์ ์ผ๋ก ์ฌ์์ฑ.- Django ORM ์ ์๋ ์ฒ๋ฆฌ๋์ง๋ง, SQLAlchemy + raw psycopg2 ๋ฑ์ ์ง์ ์ฑ๊ธธ ๊ฒ.
๋ชจ๋ ๋ก๊ทธ๋ ํธ์คํธ ์ธก ./log/ ํ๋๋ก ์ผ์ํ ๋๋ฉฐ, ์ปจํ
์ด๋ ์์์๋ /log/ ๋ก ๋ง์ดํธ๋ฉ๋๋ค.
log/
โโโ nginx/ # nginx access/error + certbot ๊ฐฑ์ ๋ก๊ทธ
โโโ gunicorn/ # gunicorn_access.log, gunicorn_error.log
โ โโโ celery/ # worker-*.log
โ โโโ celerybeat/ # celerybeat.log
โโโ uvicorn/ # uvicorn_access.log, uvicorn_error.log
โ โโโ celery/
โ โโโ celerybeat/
โโโ uwsgi/ # <project>-uwsgi.log, daemonize, _access.log
โ โโโ celery/
โ โโโ celerybeat/
โโโ daphne/ # stdout.log, error.log, access.log
โ โโโ celery/
โ โโโ celerybeat/
โโโ php-fpm/ # access.log, www-error.log, slow.log
โโโ supervisor/ # (์์ฝ ์ฌ๋กฏ)
- ์ปจํ ์ด๋๊ฐ ์ฃฝ์ด๋ ๋ก๊ทธ๋ ๋ณด์กด๋จ (ํธ์คํธ ๋ณผ๋ฅจ ๋ง์ดํธ).
- ๋ชจ๋ ํด๋๋
.gitkeep์ผ๋ก ์ถ์ (์ง์ ๋๋ ์๋ธํด๋ ํตํด). - ์ ๊ท ์๋น์ค ์ถ๊ฐ ์
log/<service>/ํด๋์.gitkeep์ ๋ฐ๋์ ์ถ๊ฐ.
- ํธ์คํธ
script/logrotate/<service>/<service>โ ์ปจํ ์ด๋/etc/logrotate.d/<service>๋ก ๋ง์ดํธ. - ์ ์ฑ
:
copytruncate๋ฐฉ์ โ ์๋น์ค ์ฌ์์ ์์ด ๋กํ ์ด์ ๊ฐ๋ฅ (๋กํ ์ด์ ์๊ฐ ๊ทน์๋ ๋ก๊ทธ ๋๋ฝ ๊ฐ๋ฅ์ฑ์ ํธ๋ ์ด๋์คํ). - ๋ณด๊ด ์ฃผ๊ธฐ: ๊ธฐ๋ณธ 30์ผ (php-fpm access๋ 7์ผ, slow log๋ 90์ผ โ ์ง๋จ ์ฐ์ ).
- ์ ์ฉ ํ์ธ:
docker exec -it <container> logrotate -d /etc/logrotate.d/<service>
docker/nginx/Dockerfileํ ๊ณณ๋ง ์ด ์ง์ค ์์ค์ ๋๋ค.script/letsencrypt.sh๋ ์ด๊ธฐ ๋ฐ๊ธ ์ ์ฉ โ cron ๋ฑ๋ก ๋ผ์ธ ์์ (์๋์ ์ ๊ฑฐ).- ๋ฑ๋ก๋ cron:
0 5 * * 1 certbot renew --quiet --deploy-hook "nginx -t && nginx -s reload" >> /log/nginx/crontab_YYYYMMDD.log 2>&1
service nginx restart/systemctl reload nginx๋ ์ฌ์ฉ ๊ธ์ง โ PID 1 = nginx ์ธ ์ปจํ ์ด๋์์๋ ๋์ ์ ํจ ๋๋ ์ ์ฒด ์ปจํ ์ด๋ ์ข ๋ฃ๋ฅผ ์ ๋ฐ.nginx -s reload= master ํ๋ก์ธ์ค์SIGHUP์ ์ก โ ์ ์์ปค spawn ํ ๊ตฌ ์์ปค graceful ์ข ๋ฃ.nginx -t &&๋ก ์ค์ ํ ์คํธ ํ์๋ง reload โ ์๋ชป๋ conf ๊ฐ ์ฆ์ prod ๋ฐ์๋๋ ์ฌ๊ณ ์ฐจ๋จ.
docker exec -it nginx-<service>-webserver bash -c "crontab -l"
docker exec -it nginx-<service>-webserver bash -c "ps aux | grep cron"
# ๋ค์ ์คํ ๋ก๊ทธ ํ์ธ
tail -f log/nginx/crontab_*.log- HTTP ์ ์ฉ nginx conf ๋ก ์ปจํ ์ด๋ ๊ธฐ๋
docker exec -it <nginx-container> bash/script/letsencrypt.sh์คํ (webroot / domain / email ์ ๋ ฅ)- ์ ์ ๋ฐ๊ธ ํ
exit - HTTPS conf ๋ก ๊ต์ฒด โ
docker compose restart
๋ฐฐ๊ฒฝ: dhparam(ssl_dhparam) ์ ๋น๋ฐ์ด ์๋๊ณ ๋๋ฉ์ธ ์ข
์๋ ์๋๋ฏ๋ก ์ ์ฒด ์คํ์์ ๋จ์ผ ๊ณต์ ๋ณธ 1 ๊ฐ ๋ฉด ์ถฉ๋ถํฉ๋๋ค. ๊ณผ๊ฑฐ์๋ ํธ์คํธ ssl/certs/ ๋ฅผ /etc/ssl/certs ๋ก ํต์งธ ๋ง์ดํธํ์ง๋ง, ์ด ๊ฒฝ๋ก๊ฐ ์์คํ
CA ๋ฒ๋ค(/etc/ssl/certs/ca-certificates.crt) ์ ๊ฐ๋ ค certbot ๋ฐ๊ธ์ด ๊นจ์ก์ต๋๋ค. ๋ณธ ๋ฒ์ ์ ์ด ์ํฐํจํด์ ์ ๊ฑฐํ๊ณ ์ด๋ฏธ์ง ๋น๋ ์์ ์ 1ํ ๊ตฝ๊ธฐ + ํธ์คํธ ๋ฐฑ์
/๋ณต์ ํ
์ผ๋ก ๋์ฒดํ์ต๋๋ค.
| ์์น | ์ญํ | ๋๊ฐ ๋ง๋๋๊ฐ |
|---|---|---|
docker/nginx/Dockerfile ์น์
8 |
openssl dhparam -out /etc/nginx/dhparam.pem 2048 โ ์ด๋ฏธ์ง์ dhparam ๊ตฝ๊ธฐ |
๋น๋ ๋จ๊ณ |
docker/nginx/Dockerfile ์น์
9 / /docker-entrypoint.d/20-dhparam.sh |
ํธ์คํธ ๋ฐฑ์ ์ด ์์ผ๋ฉด ๋ณต์, ์์ผ๋ฉด ๋ฐฑ์ | nginx ๊ธฐ๋ ์ง์ hook (๊ณต์ ์ด๋ฏธ์ง์ entrypoint.d) |
compose/web-service/<stack>/ssl/dhparam/ (ํธ์คํธ) |
๋ณต์ ์์ค / ๋ฐฑ์
๋์. /etc/nginx/dhparam-backup/ ๋ก ๋ง์ดํธ |
์ด์์ ๋๋ ์๋ ๋ฐฑ์ hook |
/etc/nginx/dhparam.pem (์ปจํ
์ด๋) |
nginx ๊ฐ ์ค์ ์ฐธ์กฐํ๋ ํ์ผ. sample_nginx_https.conf ์ ssl_dhparam ์ง์์ด๊ฐ ๊ฐ๋ฆฌํค๋ ๊ฒฝ๋ก |
hook ์ด ๋ณต์ ๋๋ ๋น๋๋ณธ ์ฌ์ฉ |
๋์ ์๋๋ฆฌ์ค:
- ์ต์ด ๊ธฐ๋ (ํธ์คํธ
ssl/dhparam/๋น์ด ์์) โ hook ์ด ์ด๋ฏธ์ง์ dhparam.pem ์ ํธ์คํธ ๋ฐฑ์ ๋๋ ํฐ๋ฆฌ๋ก ๋ณต์ฌ. ๊ฐ์ ํค๊ฐ ํธ์คํธ์ ์์ํ๋จ. docker compose downํ ์ฌ๊ธฐ๋ โ ํธ์คํธ ๋ฐฑ์ ๋ณธ์ด ์กด์ฌํ๋ฏ๋ก hook ์ด ๊ทธ ๋ฐฑ์ ๋ณธ์ ์ด๋ฏธ์ง๋ณธ ์์ ๋ฎ์ด์ฐ๊ธฐ ๋ณต์. nginx ๊ฐ ์ฒซ ๊ธฐ๋ ์์ ๋์ผํ dhparam ํค๋ฅผ ์ฌ์ฉ.- ์ด๋ฏธ์ง ์ฌ๋น๋ (์: ๋ฒ ์ด์ค nginx ๋ฒ์ bump โ ๋น๋ ์ dhparam ์ด ์๋ก ๊ตฝํ์ง) โ hook ์ด ํธ์คํธ ๋ฐฑ์
๋ณธ์ ์ฐ์ ์ ์ฉํ์ฌ ์ด์ ํค ๋์ง์ฑ ์ ์ง. ์ dhparam ์ ์๋์ ์ผ๋ก ์ฑํํ๋ ค๋ฉด ํธ์คํธ
ssl/dhparam/dhparam.pem์ ์ญ์ ํ ์ฌ๊ธฐ๋.
๊ฒ์ฆ:
# ๋น๋ ์งํ ์ด๋ฏธ์ง ์์ dhparam ์ด ๊ตฝํ๋์ง
docker run --rm devspoon-nginx:latest cat /etc/nginx/dhparam.pem | head -1
# โ "-----BEGIN DH PARAMETERS-----"
# ์ปจํ
์ด๋ ๊ธฐ๋ ํ ํธ์คํธ ๋ฐฑ์
๋ณธ ํ์ธ
ls -la compose/web-service/nginx_gunicorn/ssl/dhparam/
# โ dhparam.pem ์ด ์์ฑ๋์ด ์์ด์ผ ํจ
# ๋์ผ ํค ์ฌ์ฉ ์ฌ๋ถ ํ์ธ (๋ค์ด์ ์คํธ ๋น๊ต)
docker exec -it nginx-gunicorn-webserver sha256sum /etc/nginx/dhparam.pem
sha256sum compose/web-service/nginx_gunicorn/ssl/dhparam/dhparam.pem
# โ ๋ ๊ฐ์ด ๊ฐ์ผ๋ฉด ์ ์์์ธํ ๊ฒ์ฆ ์คํฌ๋ฆฝํธ๋ script/test_run/ssl_diag.sh ๋ฅผ ์ฌ์ฉํฉ๋๋ค (ยง10.3 ์ฐธ์กฐ).
WSL ํ๊ฒฝ ์ฃผ์ : ํธ์คํธ ๋ฐฑ์ dhparam(
ssl/dhparam/dhparam.pem) ์ด ์ปจํ ์ด๋ root ์์ ๋ก ์์ฑ๋์ด ํธ์คํธ ์ฌ์ฉ์๊ฐ ๋ณ๊ฒฝ ๋ถ๊ฐํ ์ ์์ต๋๋ค. ๋ณ๊ฒฝ/์ญ์ ์ ์ปจํ ์ด๋ ์์์ ์์ ํ๊ฑฐ๋ ํธ์คํธ์์sudo๋ก ์ฒ๋ฆฌํ์ธ์ (์์ธํ ์ ์ฐจ๋ ยง11.2 ์ฐธ์กฐ).
๊ธฐ์กด ์ ์ bad_bot.conf (500+ ํจํด ์๋ ๊ด๋ฆฌ) ๋ ํ๊ธฐ๋๊ณ , ์
์คํธ๋ฆผ์์ 6์๊ฐ ๋จ์๋ก ๊ฐฑ์ ๋๋ nginx-ultimate-bad-bot-blocker ๊ฐ ์ด๋ฅผ ๋์ฒดํฉ๋๋ค.
install-ngxblocker/setup-ngxblocker/update-ngxblocker๋ค์ด๋ก๋install-ngxblocker -x์คํ โ ์ปจํ ์ด๋ ์์ ์ด๊ธฐ ๋ฐ์ดํฐ bake:/etc/nginx/conf.d/globalblacklist.conf(๋ชจ๋ ๋ด/์ค์บ๋/์คํฌ๋ ์ดํผ map ์ ์ โ$bad_bot,$bad_referer,$validate_referer๋ฑ ๋ณ์ ์ ๊ณต)/etc/nginx/bots.d/blockbots.conf(server-level ์ฐจ๋จ ๋ก์ง)/etc/nginx/bots.d/ddos.conf(DDoS ํจํด ์ฐจ๋จ)/etc/nginx/bots.d/{blacklist,whitelist}-*.conf(์ด์์ ์ปค์คํ ์ฉ ๋น ํ์ผ)
- ์ปจํ
์ด๋ ์ cron ์ด 6์๊ฐ๋ง๋ค
update-ngxblocker์คํ โ globalblacklist.conf ๋ง ์ต์ ํ ํnginx -t && nginx -s reload(graceful reload) - ๋ฑ๋ก๋ cron ๋ผ์ธ:
0 */6 * * * /usr/local/sbin/update-ngxblocker >> /log/nginx/ngxblocker_YYYYMM.log 2>&1 && nginx -t && nginx -s reload
- certbot ๊ฐฑ์ cron ๊ณผ ๊ฐ์ ์ง์ค ์์ค(Dockerfile) โ ์ด์ ์ค ์ถ๊ฐ sudo ์์ ๋ถํ์
๊ฐ ๋๋ฉ์ธ ์๋ฒ ๋ธ๋ก์ ์ if ($bad_bot) { return 403; } ์๋ฆฌ๋ ๋ค์ 2-line include ๋ก ๋์ฒด๋ฉ๋๋ค.
ngxblocker ์ bots.d/ 9๊ฐ ํ์ผ ์ค server ์ปจํ
์คํธ์ ๋ค์ด๊ฐ๋ ๊ฑด ์ด 2๊ฐ๋ฟ:
include /etc/nginx/bots.d/blockbots.conf; # server-level `if ($bad_bot)` ๊ฒ์ฌ + 444 return
include /etc/nginx/bots.d/ddos.conf; # server-level limit_conn / limit_req (๋ด๋ง ์ ์ฉ)
โ ๏ธ bots.d/{whitelist,blacklist}-*.conf,bots.d/bad-referrer-words.conf,bots.d/custom-bad-referrers.conf๋ server ๋ธ๋ก์ ์ง์ include ํ์ง ๋ง ๊ฒ. ์ด ํ์ผ๋ค์map/geo๋ฐ์ดํฐ ํญ๋ชฉ(1.2.3.4 1;,~*pattern 1;)์ ๋ด๋ ํ์์ด๋ผ http ์ปจํ ์คํธ์ map/geo ๋ธ๋ก ์์์๋ง ์ ํจํ๋ค. server ๋ธ๋ก์ ๋ ์ฑ ์ด์์๊ฐ ํญ๋ชฉ ํ ์ค๋ง ์ถ๊ฐํด๋ ์ฆ์nginx -t์คํจ โ reload ๊ฑฐ๋ถ โ ์ด์ ๋ค์ด. globalblacklist.conf ๊ฐ http ์ปจํ ์คํธ์์ ์ด 7๊ฐ ํ์ผ์ ์๋์ผ๋ก include ํ๋ฏ๋ก ์ด์์๋ ๊ทธ๋ฅ ํด๋น ํ์ผ์ ํญ๋ชฉ๋ง ์ถ๊ฐํ๋ฉด ๋๋ค โ server ๋ธ๋ก ์์ ๋ถํ์.
์
์คํธ๋ฆผ์ด ๊ฐฑ์ ํ์ง ์๋ ์ฌ์ฉ์ ์ปค์คํ
๋ ์ด์ด โ update-ngxblocker ๊ฐ ๋ฎ์ด์ฐ์ง ์์.
ํ์ผ์ ํญ๋ชฉ๋ง ์ถ๊ฐํ๋ฉด globalblacklist.conf ๊ฐ http ์ปจํ
์คํธ์์ ์๋ ํฝ์
ํ๋ค.
server ๋ธ๋ก์ ์์ ํ์ง ์์:
| ํ์ผ | ์ฉ๋ | ํ์ ์์ |
|---|---|---|
/etc/nginx/bots.d/whitelist-ips.conf |
์ ๋ ์ฐจ๋จํ์ง ์์ IP/CIDR | 203.0.113.0/24 0; |
/etc/nginx/bots.d/whitelist-domains.conf |
์ ๋ ์ฐจ๋จํ์ง ์์ referer ๋๋ฉ์ธ | ~*example\.com 0; |
/etc/nginx/bots.d/blacklist-ips.conf |
์ถ๊ฐ ์ฐจ๋จ IP/CIDR | 198.51.100.5 1; |
/etc/nginx/bots.d/blacklist-user-agents.conf |
์ถ๊ฐ ์ฐจ๋จ User-Agent | ~*MyEvilBot 1; |
/etc/nginx/bots.d/custom-bad-referrers.conf |
์ถ๊ฐ ์ฐจ๋จ referrer ํค์๋ | ~*spam\-keyword 1; |
โ ๏ธ bots.d/blacklist-domains.conf๋ globalblacklist.conf ๊ฐ include ํ์ง ์์ผ๋ฏ๋ก ํญ๋ชฉ์ ์ถ๊ฐํด๋ ์ ์ฉ๋์ง ์๋๋ค. Dockerfile ์ด ๋น ํ์ผ๋ก touch ๋ง ํด ๋ ๋ฟ์ด๋ค. ์ถ๊ฐ ์ฐจ๋จ referer ๋๋ฉ์ธ์custom-bad-referrers.conf๋๋blacklist-user-agents.conf(UA ๊ธฐ๋ฐ) ๋ก ์ฒ๋ฆฌํ๋ผ.
์์ ํ docker exec -it nginx-<svc>-webserver bash -c "nginx -t && nginx -s reload".
# 1) ์๋ ค์ง ๋ด User-Agent ๋ก ์ฐจ๋จ๋๋์ง ํ์ธ
curl -A "MJ12bot" -o /dev/null -s -w "%{http_code}\n" http://localhost/
# โ 444 (๋๋ 403) ๊ฐ ์ ์
# 2) ์ ์ ๋ธ๋ผ์ฐ์ UA ๋ ํต๊ณผ
curl -A "Mozilla/5.0" -o /dev/null -s -w "%{http_code}\n" http://localhost/
# โ 200/3xx/4xx (502/503 ๊ฐ ์๋)
# 3) ๊ฐฑ์ ๋ก๊ทธ ํ์ธ
docker exec -it nginx-<svc>-webserver tail -20 /log/nginx/ngxblocker_$(date +%Y%m).log# 1) ์ปจํ
์ด๋ ์์์ globalblacklist.conf ์ผ์ ๋นํ์ฑํ
# (ngxblocker ๋ -c /etc/nginx ์ต์
์ผ๋ก ์ค์น๋์ด conf.d ๊ฐ ์๋๋ผ /etc/nginx/ ์ง์์ ์์.
# ๋จ์ํ ํ์ผ์ ์ฎ๊ธฐ๋ฉด nginx.conf ์ include ๋ผ์ธ์ด file-not-found ๋ก nginx -t ์คํจ โ
# include ๋ผ์ธ์ ์ฃผ์ ์ฒ๋ฆฌํ๋ ๋ฐฉ์์ด ๋ ์์ . nginx.conf ๋ ํธ์คํธ bind-mount ๋ผ
# ์ปจํ
์ด๋ ์ sed ๊ฐ ํธ์คํธ ํ์ผ๊น์ง ๊ฐฑ์ ํด ๋ค์ ๊ธฐ๋์์๋ ๋นํ์ฑ ์ ์ง.)
docker exec -it nginx-<svc>-webserver sed -i \
's|^\(\s*\)include /etc/nginx/globalblacklist.conf|\1# include /etc/nginx/globalblacklist.conf|' \
/etc/nginx/nginx.conf
docker exec -it nginx-<svc>-webserver bash -c "nginx -t && nginx -s reload"
# ๋ณต๊ท ์: git ์ผ๋ก nginx.conf ์ include ๋ผ์ธ ๋ณต์ โ reload
# 2) ๋๋ ngxblocker ๋ง์ด๊ทธ๋ ์ด์
์ด์ ์ ์ ์ bad_bot.conf ๋ฅผ git ์์ ๋ณต์
# git log --diff-filter=D -- config/web-server/nginx/gunicorn/conf.d/bad_bot.conf # ์ญ์ ์ปค๋ฐ ์๋ณ
# git show <DELETE_COMMIT>^:config/web-server/nginx/gunicorn/conf.d/bad_bot.conf > config/web-server/nginx/gunicorn/conf.d/bad_bot.conf
# ๊ทธ ๋ค์ sample_nginx*.conf ๋ฅผ git revert ๋ก ๋๋๋ฆฌ๊ณ ์ปจํ
์ด๋ conf.d ๋ก ์ฌ๋ง์ดํธ / reloadbacklog=2048 (gunicorn/uwsgi/php-fpm) ๊ฐ ์ค์ ํจ๊ณผ๋ฅผ ๋ด๋ ค๋ฉด ์ปค๋ ํ๋ผ๋ฏธํฐ๋ ํจ๊ป ์ฌ๋ ค์ผ ํฉ๋๋ค.
# /etc/sysctl.d/99-devspoon-web.conf
net.core.somaxconn = 4096
net.ipv4.tcp_max_syn_backlog = 4096
net.ipv4.ip_local_port_range = 10000 65535
vm.overcommit_memory = 1 # Redis ๊ถ์ฅ
fs.file-max = 200000
# ์ ์ฉ
sudo sysctl --system- ulimit: docker daemon ๋จ์์
default-ulimits๋กnofile=65536๊ถ์ฅ. - 8 GB RAM ํ๊ณ: swap 2-4 GB ํ๋ณด(๋ฉ๋ชจ๋ฆฌ ์คํ์ดํฌ ์ OOM ๋ฐฉ์ด์ฉ).
| ์ฆ์ | ์์ฌ ์ง์ | ๋์ |
|---|---|---|
| ์ปจํ ์ด๋ OOM Kill | ์์ปค ๋ฉ๋ชจ๋ฆฌ ํฉ > 6 GB | docker stats ๋ก RSS ์ถ์ด ํ์ธ โ workers ๋๋ max_requests ํํฅ |
| ๊ฐ์์ค๋ฌ์ด ์์ปค ์ฌ์์ | max_requests ๋๋ฌ ๋๋ harakiri/timeout |
error ๋ก๊ทธ์์ "Worker timeout" ํ์ธ. ์ฅ๊ธฐ ์์ ์ celery ๋ก ๋ถ๋ฆฌ |
| 502 Bad Gateway ๊ฐํ | upstream ์ข ๋ฃ ์์ vs nginx keepalive | gunicorn keepalive ๊ฐ nginx keepalive_timeout ๋ณด๋ค ์งง์์ง ํ์ธ |
| celery ๋ฉ๋ชจ๋ฆฌ ์ฆ๊ฐ | prefork ์์ปค ๋ฉ๋ชจ๋ฆฌ ๋์ | --max-tasks-per-child=2000 ๋์ ํ์ธ. ๋ผ์ด๋ธ๋ฌ๋ฆฌ(ํนํ numpy/pandas) ์ ๋ฉ๋ชจ๋ฆฌ fragmentaion ๊ฐ๋ฅ์ฑ |
| logrotate ๋ฏธ๋์ | ํธ์คํธ ๊ฒฝ๋ก ์คํ / cron ๋ฏธ์ค์น | script/logrotate (s ์ฃผ์) ํด๋๋ช
, ์ปจํ
์ด๋ cron ๋ฐ๋ชฌ ๋์ ํ์ธ |
| certbot ๊ฐฑ์ ์คํจ | webroot ๊ถํ / DNS ๋ณ๊ฒฝ / 80 ํฌํธ ์ฐจ๋จ | /log/nginx/crontab_*.log ํ์ธ. ์๋ dry-run: certbot renew --dry-run |
preload_app=True ํ DB ์๋ฌ |
fork ํ DB ์ปค๋ฅ์ ๊ณต์ | post_fork hook ์์ connections.close_all() (Django) ๋๋ engine ์ฌ์์ฑ |
# 1. ์ ๊ท ์ฝ๋ pull
git pull origin main
# 2. ๋ณ๊ฒฝ๋ ๋์ปคํ์ผ์ด ์์ผ๋ฉด ๋น๋ (์์ผ๋ฉด ์๋ต)
cd compose/web-service/nginx_<service>
docker compose build --no-cache <service>-app # ํ์ํ ์๋น์ค๋ง
# 3. ์ค๋จ
docker compose stop
# 4. ์์
docker compose --profile celery --profile redis up -d
# 5. ํฌ์ค ํ์ธ
docker compose ps
docker compose logs --tail=100 -f <service>-app
# 6. ์ธ๋ถ ํฌ์ค์ฒดํฌ
curl -fsS https://<domain>/health || echo "FAIL"
docker compose down์ ์ฌ์ฉํ์ง ๋ง ๊ฒ โ ์ผ๋ถ ๋คํธ์ํฌ/๋ณผ๋ฅจ ๋ฉํ๊ฐ ๊ฐ์ด ์ ๊ฑฐ๋์ด SSL/redis ๋ฐ์ดํฐ ์ฌ๊ตฌ์ฑ ๋น์ฉ์ด ๋ฐ์ํ ์ ์์ต๋๋ค.
-
.env์ ๋น๋ฐ๊ฐ (REDIS_PASSWORD,FLOWER_ID,FLOWER_PWD) ์ git ์ ์ปค๋ฐํ์ง ์์ ๊ฒ..gitignore์**/.env+!**/.env-exampleํจํด ํ์ธ (ยง0.5.1).CELERY_BROKER_URL์ .env ์ ์์ โ REDIS_PASSWORD ๋ก๋ถํฐ compose ๊ฐ ํฉ์ฑ (ยง0.5.3). -
flower(5555)ํฌํธ๋ ์ธ๋ถ ๋ ธ์ถ ์ nginx basic auth ๋๋ IP allowlist ์ ์ฉ (FLOWER_BASIC_AUTH๋ ์ด๋ฏธ ๊ฐ์ ๋์ด ์์ง๋ง ์ถ๊ฐ ๋ ์ด์ด ๊ถ์ฅ).redis-stats๋ ์ ๊ฑฐ๋์์ผ๋ฏ๋ก ๋ณ๋ ๋ชจ๋ํฐ๋ง ํ์ ์ RedisInsight/redis_exporter ๋์ (ยง0.5.7). - redis ๋ ์ปจํ
์ด๋ ๋ด๋ถ ๋คํธ์ํฌ ์ ์ฉ(์ธ๋ถ ํฌํธ ๋ฏธ๋
ธ์ถ ์ํ๊ฐ ๊ธฐ๋ณธ โ ์ ์ง ๊ถ์ฅ).
protected-mode yes+requirepass๊ฐ ๊ฐ์ ๋์ด ๋์ผ ๋คํธ์ํฌ ์ปจํ ์ด๋๋ ์ธ์ฆ ํ์ (ยง0.5.2). - Python ๋ฒ ์ด์ค ์ด๋ฏธ์ง ๋น๋ ์
docker build --build-arg PYTHON_SHA256=<official>๋๋ ARG default ๊ฐ(docker/gunicorn/Dockerfile) ์ด python.org ๊ณต์ ํด์์ ์ผ์นํ๋์ง ๋ถ๊ธฐ 1ํ ์ฌํ์ธ (ยง0.5.4). -
docker/nginx/Dockerfile์ cron ์๊ฐ(๋งค์ฃผ ์์์ผ 05:00)์ด ํธ๋ํฝ ํ์ฐ ์๊ฐ๋์ธ์ง ์ด์ ํ๊ฒฝ ๊ธฐ์ค์ผ๋ก ์ฌ๊ฒํ . - ๋ก๊ทธ ๋์คํฌ ๋ชจ๋ํฐ๋ง โ
df -h log/๊ฐ 80% ๋๋ฌ ์ ์๋ฆผ. -
ufw/ ํด๋ผ์ฐ๋ ๋ฐฉํ๋ฒฝ์์ 80/tcp, 443/tcp ๋ง ์ธ๋ถ ๋ ธ์ถ, ๊ทธ ์ธ ๋ชจ๋ ํฌํธ ์ฐจ๋จ (flower 5555 ๋ ๋ด๋ถ๋ง์์๋ง). - OS ์๊ฐ ๋๊ธฐํ(
chrony๋๋systemd-timesyncd) โ certbot/cron/๋ก๊ทธ ํ์์คํฌํ ์ ํฉ์ฑ.
๋ณธ ํ๋ก์ ํธ๋ www/django_sample ์ Python ์์กด์ฑ์ uv ๋ก ๊ด๋ฆฌํ์ง๋ง, ์ปจํ
์ด๋ ๋ด๋ถ์์๋ ์๋์ ์ผ๋ก ๋ณ๋ ๊ฐ์ํ๊ฒฝ(.venv)์ ๋ง๋ค์ง ์์ต๋๋ค. ์ปจํ
์ด๋ ์์ฒด๊ฐ ๊ฒฉ๋ฆฌ ๋จ์์ด๋ฏ๋ก venv ๋ ๋ถํ์ํ ์ค๋ณต ๊ณ์ธต์ด๋ฉฐ, ํธ๋ฌ๋ธ์ํ
์ ๋ณต์กํ๊ฒ ๋ง๋ญ๋๋ค.
docker/gunicorn/Dockerfile,docker/uwsgi/Dockerfile์ ๋ค์ ENV ๊ฐ ๋ฐํ ์์:UV_PROJECT_ENVIRONMENT=/usr/local UV_LINK_MODE=copy UV_COMPILE_BYTECODE=1 UV_NO_CACHE=1- ์ด๋ก ์ธํด ์ปจํ
์ด๋ ์์์์
uv sync๋.venv๋ฅผ ๋ง๋ค์ง ์๊ณ/usr/local/lib/python3.14/site-packages(์์คํ Python) ์ ์ง์ ์ค์น. - compose
command:๋uv run์ ๊ฑฐ์น์ง ์๊ณ ์์คํ ๋ฐ์ด๋๋ฆฌ(gunicorn,daphne,uwsgi,celery) ๋ฅผ ๊ทธ๋๋ก ํธ์ถ. --inexactํ๋๊ทธ๋ก Dockerfile ์ด ์ฌ์ ์ค์นํ ํจํค์ง(fastapi/sqlalchemy/wheel ๋ฑ) ๊ฐ ์ ๊ฑฐ๋์ง ์๋๋ก ๋ณดํธ.
| ํญ๋ชฉ | venv ์ฌ์ฉ ์ | ๋ณธ ํ๋ก์ ํธ (์์คํ ์ค์น) |
|---|---|---|
| import ๋๋ฒ๊ทธ | uv run python -c "import django" |
python -c "import django" |
| ํจํค์ง ๋ชฉ๋ก | uv pip list --python .venv/bin/python |
pip list |
| ํธ์คํธ ๋๋ ํฐ๋ฆฌ | www/<project>/.venv/ ๊ฐ ํธ์คํธ์ ์์ฑ๋จ |
ํธ์คํธ๋ ์์ค๋ง, ๊นจ๋ํ๊ฒ ์ ์ง |
| cross-volume hardlink | ์ข
์ข
์ถฉ๋ โ UV_LINK_MODE=copy ์ฐํ ํ์ |
๋์ผ layer ์์ด๋ผ ๋ฌด์ํฅ |
| ํ ์ปจํ ์ด๋์ ๋ venv | ๊ฐ๋ฅ, ํผ๋ | ๋จ์ผ ์์คํ site-packages โ ๋ชจํธํจ ์์ |
UV_PROJECT_ENVIRONMENT ๊ฐ ํธ์คํธ์๋ ์์ผ๋ฏ๋ก, ๊ฐ๋ฐ์๋ cd www/django_sample && uv sync ๋ง์ผ๋ก ์๋์ผ๋ก .venv ๊ฐ ๋ง๋ค์ด์ง๋๋ค. ํธ์คํธ์ ์ปจํ
์ด๋๊ฐ ๊ฐ์ pyproject.toml ์ ์ฐ์ง๋ง ์ค์น ์์น๋ง ๋ค๋ฅด๊ฒ ๊ฐ์ ธ๊ฐ๋๋ค.
# ํธ์คํธ(๊ฐ๋ฐ ๋จธ์ )์์:
cd www/django_sample
uv add django-celery-beat # ๋ฐํ์ deps ์ถ๊ฐ โ pyproject.toml + uv.lock ๊ฐฑ์
uv add --dev pytest-mock # ๊ฐ๋ฐ deps ์ถ๊ฐ
uv lock # ๋ฝ๋ง ์ฌ์์ฑ (ํ์ ์)
# ๋ณ๊ฒฝ ์ฌํญ์ ์ปค๋ฐ:
git add pyproject.toml uv.lock
git commit -m "deps: add django-celery-beat"
# ์ปจํ
์ด๋ ์ฌ๊ธฐ๋ โ ์์ ์์ ์ uv sync ๊ฐ ์๋ ์คํ๋์ด ์์คํ
Python ์ ๋ฐ์:
docker compose stop && docker compose up -d# 1) ์ปจํ
์ด๋ ์์์ ํจํค์ง ์ค์น ์ํ ํ์ธ โ venv ํ์ฑํ ๋ถํ์
docker exec -it gunicorn-app pip list | grep -i django
# 2) Django ํ๊ฒฝ์์ ์ฆ์ ORM ์
ธ ์ง์
docker exec -it gunicorn-app python manage.py shell
# 3) uv sync ๊ฐ ์ค์ ๋ก ์ด๋์ ์ค์นํ๋์ง ํ์ธ
docker exec -it gunicorn-app uv pip list --system
docker exec -it gunicorn-app python -c "import django; print(django.__file__)"
# โ /usr/local/lib/python3.14/site-packages/django/__init__.py๊ฐ์ compose ์คํ์ ๋ชจ๋ ํ์ด์ฌ ์ปจํ
์ด๋(app + celery + celerybeat) ๋ ๋ฐ๋์ ๋์ผํ extras ์กฐํฉ ์ผ๋ก uv sync ํฉ๋๋ค. ์์คํ
site-packages ๊ฐ ๊ณต์ ๋๋ฏ๋ก, ํ ์ปจํ
์ด๋๊ฐ ๋ค๋ฅธ extras ๋ก sync ํ๋ฉด ๋ค๋ฅธ ์ปจํ
์ด๋์ ํจํค์ง๊ฐ ์ ๊ฑฐ๋ ์ ์์ต๋๋ค. --inexact ๊ฐ 1์ฐจ ์์ ๋ง์ด์ง๋ง extras ์กฐํฉ์ ์ผ๊ด๋๊ฒ ์ ์งํ์ธ์.
| ๋ณ๊ฒฝ | ์ฌ๋น๋ ํ์? | ์ปจํ ์ด๋ ์ฌ๊ธฐ๋ ํ์? |
|---|---|---|
docker/<service>/Dockerfile |
โ
docker compose build |
โ |
config/app-server/*/... (conf.py, ini) |
โ (๋ณผ๋ฅจ ๋ง์ดํธ) | โ (์์ปค ์ฌ๋ก๋) |
config/web-server/nginx/... (๋ณ๋ ๊ด๋ฆฌ) |
โ | nginx ์ปจํ
์ด๋๋ง nginx -s reload |
compose/.../docker-compose.yml |
๋ณ๊ฒฝ ์ข ๋ฅ์ ๋ฐ๋ผ | โ |
script/logrotate/* |
โ | โ (๋ค์ cron tick ๋ถํฐ ์ ์ฉ) |
script/letsencrypt.sh |
โ | โ |
script/test/* |
โ | โ (ํธ์คํธ ์๋ ์คํ ๊ฒ์ฆ ์์ฐ, ์ด์ ๋ฌด๊ด) |
script/test/ ์ ์คํฌ๋ฆฝํธ๋ ์ด์ ์ด๋ฏธ์ง/์คํ๊ณผ ๋ฌด๊ดํฉ๋๋ค. Dockerfile / docker-compose ์ด๋ ๊ณณ์์๋ ์ฐธ์กฐ๋์ง ์์ผ๋ฉฐ ์ปจํ
์ด๋ ์์ ๋ค์ด๊ฐ์ง๋ ์์ต๋๋ค. ๊ฐ๋ฐ์๊ฐ ํธ์คํธ(WSL2 Ubuntu + Docker ๊ฐ์ )์์ ์ง์ ์คํํด ์ ํฉ์ฑ๊ณผ ํ๊ท๋ฅผ ๊ฒ์ฆํ๋ ์์ฐ์
๋๋ค. ํด๋๋ฅผ ์ญ์ ํด๋ ์ด์ ์๋น์ค ๋์์๋ ์ํฅ์ด ์์ต๋๋ค โ ํ๊ท ๊ฒ์ฆ ํธ์ ์์ฐ์ผ ๋ฟ์
๋๋ค.
| ํ์ผ | ์ข ๋ฅ | ์์ | ์ปจํ ์ด๋ ๋ณ๊ฒฝ? |
|---|---|---|---|
preflight.sh |
ํ๊ฒฝ ์ฌ์ ์ ๊ฒ (read-only) | ~30 ์ด | ์์ |
verify-ngxblocker.sh |
ngxblocker ์ข ๋จ๊ฐ ์๋ ๊ฒ์ฆ | 1โ3 ๋ถ | gunicorn ์คํ์ down โ up + ์์ conf ์ถ๊ฐ/์ ๊ฑฐ (์คํฌ๋ฆฝํธ๊ฐ ์์ฒด cleanup) |
ํ ์คํธ ๋๋ ์ด์ ์ ์ ์์ ์ ํธ์คํธ ํ๊ฒฝ์ด ์์ ์๊ฑด์ ๋ง์กฑํ๋์ง๋ฅผ 30 ์ด ์์ ํ์ธํ๋ read-only ์คํฌ๋ฆฝํธ์ ๋๋ค. ์ด๋ค ํ์ผ๋ ๋ง๋ค๊ฑฐ๋ ๋ณ๊ฒฝํ์ง ์๊ณ , ์ปจํ ์ด๋๋ ๋์ฐ์ง ์์ต๋๋ค.
์ฌ์ฉ ๋ฐฉ๋ฒ
cd /path/to/devspoon-web # ๋ฆฌํฌ์งํ ๋ฆฌ ๋ฃจํธ
bash script/test/preflight.sh์ข
๋ฃ ์ฝ๋: 0 = PREFLIGHT PASS(ํ
์คํธ/๋ฐฐํฌ ์์ ๊ฐ๋ฅ), 1 = PREFLIGHT FAIL(๋ฏธ์ถฉ์กฑ ํญ๋ชฉ ์กด์ฌ โ ํ๋ฉด์ [MISS] ๋ผ์ธ ์์ ํ ์ฌ์คํ).
์ธ์ ์คํํ๋๊ฐ
- ์ dev ํ๊ฒฝ(WSL2 / ํด๋ผ์ฐ๋ VM) ์ ์ ์งํ โ ํ์ ๋๊ตฌ๊ฐ ๋น ์ง์ง ์์๋์ง ํ์ธ.
- OS / Docker ๋ฉ์ด์ ์
๋ฐ์ดํธ ์งํ โ
docker compose v2๋ง์ด๊ทธ๋ ์ด์ ๊ฐ์ ํ๊ท ์ ๊ฒ. - ์ ๊ท ํ์ ์จ๋ณด๋ฉ ์ฒซ 30 ๋ถ โ "์ ์ ๋ผ์?" ๋ผ์ด๋ํธ๋ฆฝ์ ์ค์.
- CI ์ํฌํ๋ก์ ์ฒซ step ์ผ๋ก ํธ์ถ โ ์์กด์ฑ ๊ฐ์ํ (์ด ์คํฌ๋ฆฝํธ๊ฐ ํ๊ฒฝ ๋ฌธ์ ์ญํ ).
๋ฌด์์ ํ์ธํ๋๊ฐ (4๊ฐ ์นดํ ๊ณ ๋ฆฌ)
| ์นดํ ๊ณ ๋ฆฌ | ํญ๋ชฉ ์์ | ์คํจ ์ |
|---|---|---|
[1] Required tools |
docker(>=24), docker compose(v2), jq, curl, openssl, wrk(optional) |
[MISS] โ ํด๋น ๋๊ตฌ ์ค์น ํ์ |
[2] Repository files |
5๊ฐ ์คํ ๊ฐ .env(gunicorn / uvicorn / daphne / uwsgi / nginx_php-7.3 / nginx_php-8.4), Dockerfile 4์ข
(php-fpm ์ 7.3/8.4 ๋ ๋ค), entrypoint, pyproject.toml, script/letsencrypt.sh |
[MISS] โ ๋ฆฌํฌ์งํ ๋ฆฌ ๋ฌด๊ฒฐ์ฑ ๊นจ์ง. clone ์ฌ์๋ ๋๋ git status ํ์ธ |
[3] Design invariants |
script/logrotate ํด๋๋ช
(์คํ 'loglotate' ์๋), log/.gitkeep ร 11, pyproject.toml PEP 621 ์ฌ๋ถ, UV_PROJECT_ENVIRONMENT=/usr/local, FROM ๋ฒ ์ด์ค ์ ํฉ์ฑ, service nginx restart ํ๊ท ๋ถ์ฌ, uwsgi.ini py-autoreload=0 ๋ฑ |
[MISS] โ ์๋๋ ๋์์ธ ๊ฒฐ์ ๊นจ์ง. ์์ธํ ๊ทผ๊ฑฐ๋ ยง0, ยง6, ยง8 ์ฐธ์กฐ |
[4] Host environment |
WSL2 ์ฌ๋ถ, ๋์คํฌ ์ฌ์ 20 GB+, net.core.somaxconn 4096+ |
[WARN] โ informational. ์ด์ ์ ๊ถ์ฅ์ด์ง๋ง dev ์์๋ ๋ฌด์ ๊ฐ๋ฅ |
[3] ์ service nginx restart ๊ฒ์ฌ๋ ์ปจํ
์ด๋ ์์์ service ๋ช
๋ น์ผ๋ก nginx ๋ฅผ restart ํ๋ฉด PID 1 = nginx ์ธ ํ๊ฒฝ์์ ์ปจํ
์ด๋๊ฐ ํต์งธ๋ก ์ข
๋ฃ๋๋ ์ฌ๊ณ ๋ฅผ ๋ฐฉ์งํ๊ธฐ ์ํ ์ ์ grep ์
๋๋ค.
nginx-ultimate-bad-bot-blocker ์ ๋ค์ด๋ก๋, ํตํฉ, ์ฐจ๋จ ๋์์ด ์ค์ ๋ก ์๋ํ๋์ง๋ฅผ ์ข
๋จ๊ฐ์ผ๋ก ์๋ ๊ฒ์ฆํฉ๋๋ค. gunicorn ์คํ(compose/web-service/nginx_gunicorn)์ ํ
์คํธ ๋ฒ ๋๋ก ์ฌ์ฉํฉ๋๋ค โ PHP ์คํ์ ์ฌ์ฉํ์ง ์์ต๋๋ค(์ด์ฐจํผ nginx ์ปจํ
์ด๋ ์ด๋ฏธ์ง๋ ๋ชจ๋ ๋์ผํ๋ฏ๋ก ํ ์คํ์์๋ง ๊ฒ์ฆํด๋ ์ถฉ๋ถ).
์ฌ์ฉ ๋ฐฉ๋ฒ
cd /path/to/devspoon-web # ๋ฆฌํฌ์งํ ๋ฆฌ ๋ฃจํธ
bash script/test/verify-ngxblocker.sh์ข
๋ฃ ์ฝ๋: 0 = ALL CHECKS PASSED, 1 = ํ ๊ฐ ์ด์ ์คํจ(ํ๋ฉด์ [FAIL] ๋ผ์ธ ํ์ธ). PASS ์ ๋ง์ง๋ง ์ค์ ๋
น์ ALL CHECKS PASSED ๊ฐ ์ถ๋ ฅ๋ฉ๋๋ค.
์ค์ํ ๋ถ์์ฉ โ ์คํ ์งํ ์ํ
์ด ์คํฌ๋ฆฝํธ๋ read-only ๊ฐ ์๋๋๋ค. ๋ค์ ์์๋ก ํ๊ฒฝ์ ๋ง์ง๋๋ค:
- gunicorn ์คํ์
docker compose down -v --remove-orphansํup -d webserver redisโ ๊ฒ์ฆ์ ์ํด ๊นจ๋ํ ์ํ์์ ์์. ๊ธฐ์กด์ ๋ค๋ฅธ gunicorn ์ปจํ ์ด๋๊ฐ ๋ ์์๋ค๋ฉด ์ข ๋ฃ๋๋ฉฐ, ๋ณผ๋ฅจ๋ ํจ๊ป ์ ๊ฑฐ๋ฉ๋๋ค. - ์ปจํ
์ด๋ ์์ ์์ conf (
/etc/nginx/conf.d/zz_blocker_test.conf) ์ถ๊ฐ โHost: blocker.test๋งค์นญ server ๋ธ๋ก. ์คํฌ๋ฆฝํธ ์ข ๋ฃ ์ง์ ์Cleanup๋จ๊ณ์์ ์ ๊ฑฐ + reload ํฉ๋๋ค. - ์์ ๋ก๊ทธ ํ์ผ (
/log/nginx/blocker_test_access.log,blocker_test_error.log) โ ํธ์คํธ ๋ณผ๋ฅจ์ ๋จ์ต๋๋ค (ํ์ ์ ์๋ ์ ๋ฆฌ). - ์๋
update-ngxblocker -c /etc/nginxํธ์ถ โ globalblacklist.conf ๋ฅผ ์ต์ ํํ๋ฉฐ mtime ์ด ๊ฐฑ์ ๋ฉ๋๋ค.
์ด์ ์ค์ธ ํธ์คํธ์์ ์ง์ ๋๋ฆฌ๋ฉด ์ผ์์ ์ผ๋ก gunicorn ์คํ ๋ค์ดํ์์ด ๋ฐ์ํ๋ฏ๋ก ์ด์ ํธ๋ํฝ์ด ์๋ ์๋ฒ์์๋ ์ ๋น ์๊ฐ๋์๋ง ์คํํ์ธ์.
์ธ์ ์คํํ๋๊ฐ
| ํธ๋ฆฌ๊ฑฐ | ์ด์ |
|---|---|
nginx.conf ์ limit_conn_zone / limit_req_zone($bot_iplimit) ๊ฐ์ zone ์ด๋ฆยทkeyยทsizeยทrate ๋ณ๊ฒฝ |
rate-limit ์ ์ฑ ์ด ๋ด ์ฐจ๋จ ํตํฉ์ ํ๊ท๋ฅผ ์ผ์ผํฌ ์ ์์ |
bots.d/ddos.conf ๋๋ globalblacklist.conf ์ ์๋ update-ngxblocker ์คํ ์งํ |
์๋ cron(6์๊ฐ) ์ธ ์๋ ๊ฐฑ์ ์ ํ๊ท ๊ฐ๋ฅ์ฑ์ด ํผ |
sample_nginx*.conf ์ ngxblocker include ๋ผ์ธ(blockbots.conf / ddos.conf) ์์น ๋ณ๊ฒฝ ๋๋ ๋ค๋ฅธ bots.d/* ํ์ผ ์ถ๊ฐ |
server ์ปจํ ์คํธ include ๊ฐ ์ฌ๋ฐ๋ฅธ ์์น์ ๋ค์ด๊ฐ๋์ง ํ์ธ |
docker/nginx/Dockerfile ์ฌ๋น๋ (install-ngxblocker, ca-certificates, cron ์
์
๋ฑ ๋จ๊ณ ๋ณ๊ฒฝ) |
๋น๋ ์์ bake-in ์ฐ์ถ๋ฌผ์ด ๋ชจ๋ ์กด์ฌํ๋์ง ํ์ธ |
| 6 ๊ฐ์ ์ด์ ๋น๋/๊ฒ์ฆ ๊ณต๋ฐฑ ํ ์ฌ๊ฐ๋ | ์
์คํธ๋ฆผ(mitchellkrogza/nginx-ultimate-bad-bot-blocker) ์ ํฌ๋งท์ด ๋ฏธ์ธํ๊ฒ ๋ฐ๋์ด grep ํจํด์ด ๊นจ์ง ์ ์์ |
๊ฒ์ฆ ๋จ๊ณ (Step AโE)
| Step | ๋ฌด์์ ๋ณด๋๊ฐ |
|---|---|
| A ์คํ ๊ธฐ๋ | docker compose down -v โ up -d webserver redis, nginx -t ํต๊ณผ |
| B ๋ค์ด๋ก๋ ์ฐ์ถ๋ฌผ | globalblacklist.conf โฅ 400 KB, 1000+ ๋ด regex ํจํด, ์๋ ค์ง ๋ด 8 ์ข
(MJ12, Ahrefs, Semrush, DotBot, BLEX, Scrapy, nikto, sqlmap) ํฌํจ, bots.d/ 9 ํ์ผ ๋ชจ๋ ์กด์ฌ |
| C nginx ํตํฉ | nginx.conf ๊ฐ globalblacklist.conf ๋ฅผ 1ํ include, $bad_bot ๋ณ์ ์ ์, nginx -t syntax/test OK, master + worker โฅ 2 |
| D cron / ๊ฐฑ์ | crontab ์ update-ngxblocker -c /etc/nginx ๋ผ์ธ ๋ฑ๋ก, cron ๋ฐ๋ชฌ ์คํ, ์๋ update ์คํ ํ ํ์ผ ์ ์ + reload ํ ์์ปค ์ ์ |
| E ์ข ๋จ๊ฐ ์ฐจ๋จ | ์์ server ๋ธ๋ก(blocker.test) ์ถ๊ฐ ํ โ Mozilla UA โ 200, ๋ด UA 4์ข
(MJ12 / Ahrefs / Semrush / BLEX) โ 444 / 000 / 403, bad referer(semalt.com) ์ฐจ๋จ(์ต์
), ์ก์ธ์ค ๋ก๊ทธ ๊ธฐ๋ก |
E-4 ์ ๋ด UA ์๋ต ์ฝ๋๊ฐ 444 ๊ฐ ์๋๋ผ 000 ์ผ๋ก ๋ณด์ด๋ ๊ฒฝ์ฐ๊ฐ ์๋๋ฐ, ์ด๋ nginx ๊ฐ ์๋ต ์์ด ์ฐ๊ฒฐ์ ๋ซ์ curl ์ด ์๋ต์ ๋ชป ๋ฐ์๋ค๋ ์๋ฏธ๋ก ๋์ผํ PASS ์
๋๋ค.
| ์๋๋ฆฌ์ค | ์คํ ๋ช ๋ น |
|---|---|
| ์ dev ํ๊ฒฝ ์ ์ ํ ์ฒซ ์ง๋จ | bash script/test/preflight.sh |
| OS / Docker ์ ๋ฐ์ดํธ ์งํ ํ๊ท ์ ๊ฒ | bash script/test/preflight.sh |
nginx.conf ์ rate-limit / bots.d/* ๋ณ๊ฒฝ ํ |
bash script/test/verify-ngxblocker.sh |
docker/nginx/Dockerfile ์ฌ๋น๋ ํ |
bash script/test/verify-ngxblocker.sh |
| ๋ ์คํฌ๋ฆฝํธ๋ฅผ ์ฐ๋ฌ์ (์ ์ โ ๋ด ์ฐจ๋จ ๊ฒ์ฆ) | bash script/test/preflight.sh && bash script/test/verify-ngxblocker.sh |
์๋ก์ด ํ๊ท ๊ฒ์ฆ ์์ฐ์ด ํ์ํด์ง๋ฉด ๊ฐ์ ํด๋์ verify-<topic>.sh ๋๋ preflight-<topic>.sh ๋ค์ด๋ฐ์ผ๋ก ์ถ๊ฐํ๋ฉด ์ผ๊ด์ฑ์ด ์ ์ง๋ฉ๋๋ค.
ยง10 ์ script/test/ ๊ฐ "ํน์ ์์ญ๋ง ์ ์ ๊ฒ์ฆ" ์ธ ๋ฐ ๋นํด, script/test_run/ ์ ๋จ๊ณ ๋ฒํธ (s0/s1b/s2/s3/s5/s6) ๋ก ์ ๋ ฌ๋ ์ข
๋จ๊ฐ ํ๊ท ๋ฐฐํฐ๋ฆฌ ์
๋๋ค. aisum-infrakit ์ ๊ฒ์ฆ ์์ฐ์ ๋ณธ ํ๋ก์ ํธ๋ก ์ญ์ด์ํ์ฌ ๋์ผํ ํ๊ท ์๋๋ฆฌ์ค๋ฅผ devspoon-web ์์๋ ์คํํ ์ ์๊ฒ ํ์ต๋๋ค.
| ๋จ๊ณ | ์คํฌ๋ฆฝํธ | ๊ฒ์ฆ ์์ญ | ๋น๊ณ |
|---|---|---|---|
| s0 | s0_prereq.sh |
docker / docker compose ๋ฒ์ , ํธ์คํธ ํฌํธ(80/443/5555) ๊ฐ์ฉ, log/<service>/ ์กด์ฌ, uv ์ค์น, www/django_sample uv sync |
์ฌ์ ์ ๊ฒ (read-only ๊ฐ๊น์) |
| s1b | s1b_exit_check.sh |
์ปจํ ์ด๋ ๋น์ ์ ์ข ๋ฃ ์ exit code / ๋ก๊ทธ ํจํด ๊ฒ์ฌ | ์ง๋จ์ฉ |
| s1b | s1b_nginx_conf_generators.sh |
5๊ฐ ์คํ(gunicorn / uvicorn / uwsgi / php-7.3 / php-8.4) ์ nginx_http_conf.sh / nginx_https_conf.sh ๊ฐ ์ ์ ์ฐ์ถ๋ฌผ์ ๋ง๋๋์ง โ ์นํ ๋๋ฝ, ๋น์ด ์๋ placeholder, ํ์ผ ๊ถํ ๊ฒ์ฌ |
conf ์์ฑ๊ธฐ ํ๊ท |
| s2 | s2_build.sh |
์คํ๋ณ Dockerfile ์ docker build -f ๋ก ๊ฒฉ๋ฆฌ ๋น๋ (php-fpm ์ 7.3/8.4 ๋ ๋ณํ ๋ชจ๋), aisum-test/* ํ๊ทธ๋ก ์ฐ์ถ |
๋น๋ ํ๊ท. compose layer ์ ๋ฌด๊ด |
| s2a | s2a_image_inspect.sh |
๋น๋๋ ์ด๋ฏธ์ง์ baselayer ์ ํฉ, ENV / WORKDIR / CMD ๊ฒ์ฌ | ์ด๋ฏธ์ง ๋ฉํ |
| s3 | s3_stack_smoke.sh <stack> <appname> <appcontainer> <stack_name> |
๋จ์ผ ์คํ ๊ธฐ๋ โ nginx -t โ curl -H "Host: ..." ์๋ต 200 ์ฌ๋ถ โ cleanup |
per-stack smoke |
| s5 | s5_https.sh <stack> |
HTTPS ์ธก: dhparam ์์ฑ/๋ง์ดํธ/๋ณต์, self-signed cert ์์ฑ, sample_nginx_https.conf ์นํ ์ฐ์ถ๋ฌผ ๊ฒ์ฆ, nginx -t ํต๊ณผ โ certbot ๋ฐ๊ธ์ ์ ์ธ (๋๋ฉ์ธ ์๋ ํ๊ฒฝ ๊ฐ์ ) |
HTTPS ์ ํฉ์ฑ |
| s6 | s6_regression.sh |
ํตํฉ ํ๊ท โ ์ ๋ชจ๋ ๋จ๊ณ๋ฅผ ์์๋๋ก ํธ์ถ ํ ์ข ํฉ ๊ฒฐ๊ณผ ์ถ๋ ฅ | ์ผ๊ฐ ํ๊ท |
| ๋ณด์กฐ | ssl_diag.sh |
dhparam ๊ฒฝ๋ก/๋ด์ฉ ๊ฒ์ฌ + ํธ์คํธ ๋ฐฑ์ ๋ณธ โ ์ปจํ ์ด๋ ๋ณธ ์ผ์น ๊ฒ์ฌ | ยง3 dhparam ์์ํ ์ ์ ์๋ํ ๊ฒ์ฆ |
| ๋ณด์กฐ | verify_block.sh |
๋ด/์ค์บ๋ ์ฐจ๋จ ๋์ ๊ฒ์ฌ (ยง10.2 ์ verify-ngxblocker ์ ์์ญ ์ผ๋ถ ์ค๋ณต) | |
| ๋ณด์กฐ | verify_compose_yml.sh |
6 ์คํ docker-compose.yml ์ dhparam ๋ง์ดํธ / ์ํฐํจํด (ssl/certs ๋ง์ดํธ, ulimits ๋ฏธ์ ์) ์ ์ ๊ฒ์ฌ | ์ ์ ํ๊ท |
| ๋ณด์กฐ | verify_conf_generators.sh |
4 ์คํ ร HTTP+HTTPS generator ์ฐ์ถ๋ฌผ ๊ฒ์ฆ | ์ ์ ํ๊ท |
| ๋ณด์กฐ | verify_dhparam_lifecycle.sh / verify_dhparam_host_wins.sh |
dhparam A/B/C ๋จ๊ณ ๋ฐฑ์ ยท๋ณต์ยทํธ์คํธ ์ฐ์ ๊ฒ์ฆ (PORT ๋๋คํ + ํด๋ง ๊ฐํ) | ยง3 dhparam |
| ๋ณด์กฐ | verify_healthcheck.sh |
6 ์คํ healthcheck ์ ์ 12 PASS + ๋ฐํ์ ์ต์ | ยง0.5 healthcheck |
| ๋ณด์กฐ | verify_nginx_standalone.sh |
์ค์ nginx ์ปจํ
์ด๋ ๊ธฐ๋ + ์ ์ฒด ๋ง์ดํธ + docker cp ๋ก ์ข
๋ฃ ์ปจํ
์ด๋์์๋ dhparam ์ถ์ถ |
dhparam ํตํฉ |
| ํตํฉ | verify_integration_<stack>.sh ร 6 |
6 ์คํ ํ์คํ ํตํฉ verifier โ .env ์๋ ์
์
+ docker compose up -d + healthcheck ๋๊ธฐ + curl Host: localhost HTTP 200 + gzip Vary + ์์ปค ๊ถํ ๊ฐํ (master root + workers www-data) + dhparam sha256 ์ ํฉ ๊ฒ์ฆ + ์๋ cleanup |
gunicorn / uvicorn / uwsgi / daphne / php73 / php84 |
| ๋ณด์กฐ | celery_diag.sh / check_cgi.sh / check_cgi2.sh / check_excode.sh / inspect_orphans.sh / sim_exit.sh |
๊ฐ๋ณ ์ง๋จ ๋ณด์กฐ | ๋จ๋ฐ์ฑ |
# ๋จ๊ณ๋ณ ์คํ (s0 โ s2 โ s3 โ s5 โ s6 ์)
cd /mnt/c/Users/rnd15/Documents/project/github/mig/devspoon-web
bash script/test_run/s0_prereq.sh
# ๋จ์ผ ์คํ smoke (gunicorn)
bash script/test_run/s3_stack_smoke.sh nginx_gunicorn gunicorn gunicorn-app gunicorn
# ๋จ์ผ ์คํ HTTPS ๊ฒ์ฆ (๋๋ฉ์ธ ์์ด, certbot ์ ์ธ)
# ์ธ์: STACK_DIR STACK WEBROOT APPNAME SERVICE_PORT
bash script/test_run/s5_https.sh nginx_gunicorn gunicorn django_sample gunicorn-app 8000
# dhparam ์์ํ ๊ฒ์ฆ (ยง3 dhparam ์ ์ ์๋ํ)
bash script/test_run/ssl_diag.sh
# ์ ์ฒด ํ๊ท
bash script/test_run/s6_regression.sh
# === ํ์คํ ํตํฉ verifier (6 stack, end-to-end) ===
# .env ์๋ ์
์
+ compose up + healthcheck ๋๊ธฐ + HTTP 200 + gzip + ๊ถํ ๊ฐํ + dhparam ๊ฒ์ฆ
bash script/test_run/verify_integration_gunicorn.sh
bash script/test_run/verify_integration_uvicorn.sh
bash script/test_run/verify_integration_uwsgi.sh
bash script/test_run/verify_integration_daphne.sh
bash script/test_run/verify_integration_php73.sh
bash script/test_run/verify_integration_php84.sh- ๋ชจ๋ ์คํฌ๋ฆฝํธ๋
ROOT="/mnt/c/Users/rnd15/Documents/project/github/mig/devspoon-web"๋ฅผ ํ๋์ฝ๋ฉํ๊ณ ์์ต๋๋ค. ๋ค๋ฅธ ๊ฒฝ๋ก์์ ์ฌ์ฉํ๋ ค๋ฉด ์ฒซ ์ค์ROOT=๋ณ์๋ฅผ ์์ ํ์ธ์. s5_https.sh๋ ๋๋ฉ์ธ์ด ์๋ ๋ก์ปฌ ํ๊ฒฝ ๊ฐ์ โ certbot ๋ฐ๊ธ์ ์๋ํ์ง ์๊ณ dhparam / nginx https ์ํ / ๊ฒฝ๋ก ์ ํฉ์ฑ๋ง ๊ฒ์ฆํฉ๋๋ค.s3_stack_smoke.sh๋ ์ปจํ ์ด๋๋ฅผ ๋์ ๋ค ๋ด๋ฆฌ๋ฏ๋ก ์ด์ ํธ์คํธ์์๋ ์ ๋น ์๊ฐ๋์๋ง ์คํ.- WSL2 ํธ์คํธ์ธ ๊ฒฝ์ฐ
script/test_run/*.sh์คํ ์ง์ ์chmod 644 compose/web-service/*/redis/conf/redis.conf(๋ฐ ๊ธฐํ bind-mount ๋์) ๊ถํ์ ์ฌํ์ธํด์ผ ํ ์ ์์ต๋๋ค. WSL ์ ๊ธฐ๋ณธfmask=177์ ์ฑ ์ผ๋ก ์ธํด 0600 ์ผ๋ก ์๋ฆฌ๋ฉด ์ปจํ ์ด๋ ๋ด๋ถ redis ๊ฐ conf ๋ฅผ ์ฝ์ง ๋ชปํฉ๋๋ค. ์๊ตฌ ํํผ์ฑ ์ ยง11 (WSL2 ํธ์คํธ ์ด์ ๊ฐ์ด๋) ์/etc/wsl.conf์ค์ ์ ์ฐธ์กฐ.
๋ณธ ํ๋ก์ ํธ๋ dev ํ๊ฒฝ์์ Windows + WSL2 (Ubuntu) ์์ docker ๋ฅผ ๋์ฐ๋ ์๋๋ฆฌ์ค๋ฅผ ๊ด๋ฒ์ํ๊ฒ ๊ฐ์ ํฉ๋๋ค (test_run ์คํฌ๋ฆฝํธ์ ROOT=/mnt/c/... ํ๋์ฝ๋ฉ์ด ๊ทธ ํ์ ). WSL2 ์ ๊ธฐ๋ณธ mount ์ต์
์ด ์ปจํ
์ด๋ bind-mount ๊ฒฝ๋ก์ ๊ถํ์ ์๋ผ ์ด์์ ๊นจ๋จ๋ฆฌ๋ ์ผ์ด์ค๊ฐ ๋ฐ๋ณต๋๋ฏ๋ก ์ด ์ ์์ ์ ๋ฆฌํฉ๋๋ค.
์ด์(production) ํ๊ฒฝ์ ๊ฐ๋ฅํ๋ฉด Linux native ๋๋ ํด๋ผ์ฐ๋ VM์ ์ฌ์ฉํ์ธ์. WSL2 ๋ dev / ๊ฒ์ฆ ์ฉ๋์ ๋๋ค.
WSL2 ์ ๊ธฐ๋ณธ /mnt/c ๋ง์ดํธ๋ fmask=177 (์ฆ 0600 โ ์์ ์ r/w ๋ง ํ์ฉ) ๋ก ๋์ํฉ๋๋ค. ์ด ์ํ์์๋ ์ปจํ
์ด๋๊ฐ redis.conf (0644 ํ์), www/php_sample/index.php (0644 ํ์), nginx.conf ๋ฑ bind-mount ํ ๋ชจ๋ ํ์ผ์ ์ฝ์ง ๋ชปํด ์ฆ์ ์ข
๋ฃ๋ฉ๋๋ค ("Permission denied" / "Failed to open log file" ๋ฑ).
WSL2 ์ธ์คํด์ค์ /etc/wsl.conf ์ ๋ค์์ ์ถ๊ฐํ์ธ์ (์์ผ๋ฉด ์์ฑ).
[automount]
enabled = true
options = "metadata,umask=22,fmask=11"metadata: Linux ์ธก์์ chmod/chown ๋ฉํ๋ฐ์ดํฐ๋ฅผ Windows NTFS ์ ๋ณด๊ด ๊ฐ๋ฅํ๊ฒ ํจ.umask=22: ๋๋ ํฐ๋ฆฌ ๊ธฐ๋ณธ 0755,fmask=11: ํ์ผ ๊ธฐ๋ณธ 0644 (์ฆ mount ๋ ํ์ผ์ด 0644 ๋ก ๋ ธ์ถ).
์ค์ ํ PowerShell ์์:
wsl --shutdown
# ๊ทธ ๋ค์ WSL ํฐ๋ฏธ๋ ์ฌ์คํ โ ์ ๋ง์ดํธ ์ต์
์ ์ฉ๊ฒ์ฆ:
mount | grep '/mnt/c'
# โ ์ต์
์ "umask=22,fmask=11" ๊ฐ ๋ณด์ฌ์ผ ํจ
ls -l compose/web-service/nginx_gunicorn/redis/conf/redis.conf
# โ -rw-r--r-- (0644) ๋ก ๋ณด์ฌ์ผ ํจWSL ํ๊ฒฝ์์๋ ์ปจํ
์ด๋์ dhparam ๋ฐฑ์
hook ์ด ํธ์คํธ ๋๋ ํฐ๋ฆฌ(./ssl/dhparam/) ์ ํ์ผ์ ์ฐ๋๋ฐ, ์ปจํ
์ด๋ ๋ด root ์์ ๋ก ์์ฑ๋ฉ๋๋ค. ํธ์คํธ ๋น-root ์ฌ์ฉ์๋ ์ด ํ์ผ์ ์ง์ ์์ ํ ์ ์์ต๋๋ค. ๋ณ๊ฒฝ์ด ํ์ํ๋ฉด:
# ์ปจํ
์ด๋ ์์์ ์์
ํ๊ฑฐ๋
docker compose exec webserver sh -c 'rm /etc/nginx/dhparam-backup/dhparam.pem'
# WSL ํธ์คํธ์์ sudo ๋ก ์ฒ๋ฆฌ
sudo rm compose/web-service/nginx_gunicorn/ssl/dhparam/dhparam.pem/mnt/c ์ 9P/Plan9 ๋ง์ดํธ๋ native ext4 ๋๋น IO ๊ฐ ~10x ๋๋ฆฝ๋๋ค. dev ์ ์ปจํ
์ด๋ build ๊ฐ ๊ธธ์ด์ง๋ ์ฃผ๋ ์์ธ์ด๋ฉฐ, ์ด์ ๊ฐ์ด๋๋ผ๊ธฐ๋ณด๋ค dev ์์ฐ์ฑ ํ์
๋๋ค โ ๊ฐ๋ฅํ๋ฉด ํ๋ก์ ํธ๋ฅผ ~/projects/devspoon-web (WSL2 native ext4) ๋ก ์ฎ๊ธฐ๊ณ hardcoded ROOT ๋ง ๊ฐฑ์ .
WSL2 ์์๋ ์ปจํ
์ด๋ ์์ ~ healthcheck ์ฒซ ํ ์ฑ๊ณต๊น์ง ์๊ฐ์ด native Linux ๋๋น ๊ธธ์ด์ง ์ ์์ต๋๋ค. compose ์ start_period: 30s ๊ฐ ๋ง์ง์ ๋๊ธด ํ์ง๋ง, WSL ํธ์คํธ๊ฐ ๋ฉ๋ชจ๋ฆฌ ์๋ฐ ์ํ๋ผ๋ฉด ๋ถ์กฑํ ์ ์์ต๋๋ค. ๊ทธ ๊ฒฝ์ฐ webserver ๊ฐ dependency failed to start: container ... is unhealthy ๋ก ์คํจ โ ์ฒ์ ํ ๋ฒ timeout ์ 60s ์ ๋๋ก ์์ ์ํฅํ ๋ค ์ ์ํ๋๋ฉด ์๋ณตํฉ๋๋ค.
- Website : Owner's personal website is devspoon.com
- Lim Do-Hyun Owner Developer/project Manager, bluebamus@gmail.com