웹호스팅 가능하게 해주는 파이썬 스크립트
Posted: Sat Aug 02, 2025 1:58 pm
#!/usr/bin/env python3
import os
import subprocess
import zipfile
import shutil
#
MySQL root 비밀번호를 미리 하드코딩
MYSQL_ROOT_PASS = "비밀번호입력" #
보안을 위해 .env 파일이나 외부 설정으로 빼는 게 좋음
username = input("생성할 유저명을 입력하세요: ").strip() //사용자에게 유저명을 입력받는다. 유저명을 입력받으면 자동으로 모든게 다 생성된다
if not username.isalnum(): //isalnum(): 매서드는 파이썬의 문자열 매서드임 특징이
알파벳 a-z A-Z 숫자 0-9로만 이루어져 있으면 = Ture if not = False를 반환
print("영문/숫자만 입력해주세요.")
exit()
# 웹보드 선택 // 나같은 경우 /var/www/web_board 라는 폴더를 생성하여 그안에 각종 웹보드들의 압축파일을 넣었
print("\n어떤 웹보드로 계정을 생성할까요?")
print("1. phpBB3")
print("2. WordPress")
print("3. 그누보드5")
board_choice = input("번호를 입력하세요 [1/2/3]: ").strip() //분명히 1,2,3 인데 사용자가 if 4를 넣으면 Error를 출력하며 프로세스가 종료된다
if board_choice == "1": //조건문이지 그 유명한 if elif else 절
board_type = "phpbb3"
elif board_choice == "2":
board_type = "wordpress"
elif board_choice == "3":
board_type = "gnuboard5"
else:
print("잘못 입력함. 중단!")
exit()
# DB 체크/삭제
check_sql = f"SHOW DATABASES LIKE '{username}';" //자 그다음 MYSQL 의 DB의 존재유무 확인 이 코드는 계정명으로 모든것이 통일되어 있으므로
DB이름 역시 계정명으로 자동생성된다
//subprocess.run은 파이썬에서 외부 명령어를 실행하는 함수 이 함수 특징이 명령어 실행후 결과를 반환한다는 것 여기서 반환값은 Completedprocess 객체
로 그 안에는 명령어의 실행결과및 출력 및 오류 메세지등등이 포함된다
여기 코드들은 Root의 권한으로 실행된다
result = subprocess.run(['sudo', 'mysql', '-u', 'root', f'-p{DB비번}', '-e', check_sql], capture_output=True, text=True)
if username in result.stdout:
confirm = input(f"\n
이미 '{username}' 데이터베이스가 존재함. 삭제하고 새로 만들까요? (y/N): ").strip().lower()
if confirm == 'y':
drop_sql = f"DROP DATABASE `{username}`;"
subprocess.run(['sudo', 'mysql', '-u', 'root', f'-p{DB비번}', '-e', drop_sql], text=True)
else:
print("\n
생성 중단됨.")
exit()
# 리눅스 유저 체크/삭제
user_check = subprocess.run(['id', '-u', username], capture_output=True)
if user_check.returncode == 0:
confirm = input(f"\n
리눅스 유저 '{username}' 이미 존재. 삭제하고 새로 만들까요? (y/N): ").strip().lower()
if confirm == 'y':
subprocess.run(['sudo', 'userdel', '-r', username])
else:
print("
생성 중단됨.")
exit()
# 리눅스 유저 생성
try:
subprocess.run(['sudo', 'useradd', '-m', '-d', f'/home/{username}', '-s', '/bin/bash', username], check=True)
subprocess.run(['sudo', 'chpasswd'], input=f"{username}:{username}\n", text=True, check=True)
except Exception as e:
print(f"유저 생성 에러: {e}")
exit()
# public_html 생성 및 권한 각사용자의 /home/맨처음입력한 계정명/public_html안에 사용자가 선택한 웹보드의 압축파일을 임시로 푼다음 여기 경로로
복사한다.
web_dir = f"/home/{username}/public_html"
os.makedirs(web_dir, exist_ok=True)
subprocess.run(['sudo', 'chmod', '755', f'/home/{username}'])
subprocess.run(['sudo', 'chmod', '755', web_dir])
subprocess.run(['sudo', 'chown', '-R', f'{username}:{username}', f'/home/{username}'])
# DB 및 사용자 생성
db_sql = f'''
CREATE DATABASE `{username}`;
CREATE USER '{username}'@'localhost' IDENTIFIED BY '{username}'; //username 이라는 변수는 이미 값을 가지고 있다 코드 초기 입력받은 계정명이다
GRANT ALL PRIVILEGES ON `{username}`.* TO '{username}'@'localhost';
FLUSH PRIVILEGES;
'''
try:
subprocess.run(['sudo', 'mysql', '-u', 'root', f'-p{DB비번}'], input=db_sql, text=True)
print("
DB 및 사용자 생성 완료")
except Exception as e:
print(f"
DB 생성 에러: {e}")
# DNS zone 등록 // 자동으로 사용자계정명으로 서브 도메인을 Zone파일에 넣어준다
zone_file = "/etc/bind/db.wujin.monster"
subdomain_entry = f"\n{username}\tIN\tA\t183.103.115.250\n"
//subdomain_entry = f"\n{username}\tIN\tA\t183.103.115.250\n" 여기서 f" = f-string은 파이썬 3.6이상에서 도입된 문자열 포매팅 방식이다.
f로 시작되는 문자열 안에 중괄호로 {}감싸진 부분은 변수나 표현식의 값을 문자열에 삽입한다.
즉 중요한 {username}값을 코드에서 이미 정의된 username 변수의 값으로 치환된다. 즉 대체된다는 뜻 !
결국 이거지 {username} 이 {test}라고 가정하면 test.wujin.monster 서버외부IP 이렇게 서브도메인이 값이 들어간다는 것!
try:
with open(zone_file, "a") as f:
f.write(subdomain_entry)
except Exception as e:
print(f"존 파일 수정 에러: {e}")
subprocess.run(['sudo', 'systemctl', 'reload', 'bind9'])
# Apache 가상호스트 설정
apache_conf = f""" // f""" f-string은 파이썬 3.6이상에서 도입된 멀티라인 문자열 정의를 의미한다. 핵심은 문자열 내의 중괄호 {}를 이용해 변수의 값을
직접 삽입할 수 있다는 점이다 !
<VirtualHost *:80>
ServerName {username}.wujin.monster //여기서도 나왔지 중괄호 안에 유저네임 << 이 유저네임이 최초로 입력받은 사용자 계정ID랑 동일하다는것
DocumentRoot /home/{username}/public_html //도큐먼트루트의 경로를 아팟치는 어디든 리디렉션 해줄수 있어 결국 /var/www/html 안에
// 안 넣어도 된다는 거야 다른 경로도 얼마든지 할 수 있음
<Directory /home/{username}/public_html>
Options Indexes FollowSymLinks
AllowOverride All //아팟치 설정보면 이거 엄청 나오더라 .htaccess 파일을 허용할지 deny할지의 여부 리디렉션 or Url 재작성 or 접근 제어 or 보안 설정
Require all granted //mod_authz_core 아팟치 모듈인데 모든 사용자에게 허용한다는 의미 아팟치하면 또 모듈이지 개많아
</Directory>
ErrorLog /var/log/apache2/{username}_error.log
CustomLog /var/log/apache2/{username}_access.log combined
</VirtualHost>
"""
conf_path = f"/etc/apache2/sites-available/{username}.wujin.monster.conf"
// sites-available 여기안에 가상호스트 설정파일들이 즐비한곳 각각의 도메인들에 대한 설정이 포함된 곳
try:
with open(conf_path, "w") as f:
f.write(apache_conf)
subprocess.run(['sudo', 'a2ensite', f'{username}.wujin.monster.conf']) //모든 리눅스들은 일시적으로 root의 권한을 잠깐잠깐식 빌려서 명령을 처리한다
//'sudo', 'a2ensite', = 일시적으로 1행엠
subprocess.run(['sudo', 'systemctl', 'reload', 'apache2'])
except Exception as e:
print(f"Apache 설정 에러: {e}")
#
웹보드 설치 (로컬 zip 사용)
if board_type == "wordpress":
wp_zip_path = "/var/www/web_board/wordpress.zip"
with zipfile.ZipFile(wp_zip_path, "r") as zip_ref:
zip_ref.extractall(web_dir)
wp_subdir = os.path.join(web_dir, "wordpress")
if os.path.isdir(wp_subdir):
for f in os.listdir(wp_subdir):
shutil.move(os.path.join(wp_subdir, f), web_dir)
shutil.rmtree(wp_subdir)
print("
워드프레스 설치 완료!")
elif board_type == "phpbb3":
phpbb_zip_path = "/var/www/web_board/phpbb3.zip"
with zipfile.ZipFile(phpbb_zip_path, "r") as zip_ref:
zip_ref.extractall(web_dir)
phpbb_subdir = os.path.join(web_dir, "phpBB3")
if os.path.isdir(phpbb_subdir):
for f in os.listdir(phpbb_subdir):
shutil.move(os.path.join(phpbb_subdir, f), web_dir)
shutil.rmtree(phpbb_subdir)
#
phpBB3 퍼미션 자동 설정
permission_paths = [
"cache", "store", "files", "images/avatars/upload"
]
for p in permission_paths:
target = os.path.join(web_dir, p)
if os.path.exists(target):
subprocess.run(['sudo', 'chmod', '-R', '777', target])
subprocess.run(['sudo', 'chown', '-R', f'{username}:{username}', target])
#
config.php는 666
config_file = os.path.join(web_dir, "config.php")
if os.path.exists(config_file):
subprocess.run(['sudo', 'chmod', '666', config_file])
subprocess.run(['sudo', 'chown', f'{username}:{username}', config_file])
print("
phpBB3 설치 및 권한 설정 완료!")
elif board_type == "gnuboard5":
gnu_zip_path = "/var/www/web_board/gnuboard5.zip"
with zipfile.ZipFile(gnu_zip_path, "r") as zip_ref:
zip_ref.extractall(web_dir)
# 압축 풀었을 때 생성된 디렉토리 이름을 자동 추적
extracted_dirs = [d for d in os.listdir(web_dir) if os.path.isdir(os.path.join(web_dir, d))]
if len(extracted_dirs) == 1:
gnu_subdir = os.path.join(web_dir, extracted_dirs[0])
for f in os.listdir(gnu_subdir):
shutil.move(os.path.join(gnu_subdir, f), web_dir)
shutil.rmtree(gnu_subdir)
#
data 디렉토리 자동 생성 및 퍼미션 설정
data_dir = os.path.join(web_dir, "data")
os.makedirs(data_dir, exist_ok=True)
subprocess.run(['sudo', 'chmod', '707', data_dir])
subprocess.run(['sudo', 'chown', f'{username}:{username}', data_dir])
print("
그누보드5 설치 완료!")
# 퍼미션 정리
subprocess.run(['sudo', 'chown', '-R', f'{username}:{username}', web_dir])
# 완료 출력
print("\n===============================")
print(f"[완료] {username} 호스팅 계정 생성 완료!")
print(f"
접속: http://{username}.wujin.monster")
print("===============================")
import os
import subprocess
import zipfile
import shutil
#
MYSQL_ROOT_PASS = "비밀번호입력" #
username = input("생성할 유저명을 입력하세요: ").strip() //사용자에게 유저명을 입력받는다. 유저명을 입력받으면 자동으로 모든게 다 생성된다
if not username.isalnum(): //isalnum(): 매서드는 파이썬의 문자열 매서드임 특징이
알파벳 a-z A-Z 숫자 0-9로만 이루어져 있으면 = Ture if not = False를 반환
print("영문/숫자만 입력해주세요.")
exit()
# 웹보드 선택 // 나같은 경우 /var/www/web_board 라는 폴더를 생성하여 그안에 각종 웹보드들의 압축파일을 넣었
print("\n어떤 웹보드로 계정을 생성할까요?")
print("1. phpBB3")
print("2. WordPress")
print("3. 그누보드5")
board_choice = input("번호를 입력하세요 [1/2/3]: ").strip() //분명히 1,2,3 인데 사용자가 if 4를 넣으면 Error를 출력하며 프로세스가 종료된다
if board_choice == "1": //조건문이지 그 유명한 if elif else 절
board_type = "phpbb3"
elif board_choice == "2":
board_type = "wordpress"
elif board_choice == "3":
board_type = "gnuboard5"
else:
print("잘못 입력함. 중단!")
exit()
# DB 체크/삭제
check_sql = f"SHOW DATABASES LIKE '{username}';" //자 그다음 MYSQL 의 DB의 존재유무 확인 이 코드는 계정명으로 모든것이 통일되어 있으므로
DB이름 역시 계정명으로 자동생성된다
//subprocess.run은 파이썬에서 외부 명령어를 실행하는 함수 이 함수 특징이 명령어 실행후 결과를 반환한다는 것 여기서 반환값은 Completedprocess 객체
로 그 안에는 명령어의 실행결과및 출력 및 오류 메세지등등이 포함된다
여기 코드들은 Root의 권한으로 실행된다
result = subprocess.run(['sudo', 'mysql', '-u', 'root', f'-p{DB비번}', '-e', check_sql], capture_output=True, text=True)
if username in result.stdout:
confirm = input(f"\n
if confirm == 'y':
drop_sql = f"DROP DATABASE `{username}`;"
subprocess.run(['sudo', 'mysql', '-u', 'root', f'-p{DB비번}', '-e', drop_sql], text=True)
else:
print("\n
exit()
# 리눅스 유저 체크/삭제
user_check = subprocess.run(['id', '-u', username], capture_output=True)
if user_check.returncode == 0:
confirm = input(f"\n
if confirm == 'y':
subprocess.run(['sudo', 'userdel', '-r', username])
else:
print("
exit()
# 리눅스 유저 생성
try:
subprocess.run(['sudo', 'useradd', '-m', '-d', f'/home/{username}', '-s', '/bin/bash', username], check=True)
subprocess.run(['sudo', 'chpasswd'], input=f"{username}:{username}\n", text=True, check=True)
except Exception as e:
print(f"유저 생성 에러: {e}")
exit()
# public_html 생성 및 권한 각사용자의 /home/맨처음입력한 계정명/public_html안에 사용자가 선택한 웹보드의 압축파일을 임시로 푼다음 여기 경로로
복사한다.
web_dir = f"/home/{username}/public_html"
os.makedirs(web_dir, exist_ok=True)
subprocess.run(['sudo', 'chmod', '755', f'/home/{username}'])
subprocess.run(['sudo', 'chmod', '755', web_dir])
subprocess.run(['sudo', 'chown', '-R', f'{username}:{username}', f'/home/{username}'])
# DB 및 사용자 생성
db_sql = f'''
CREATE DATABASE `{username}`;
CREATE USER '{username}'@'localhost' IDENTIFIED BY '{username}'; //username 이라는 변수는 이미 값을 가지고 있다 코드 초기 입력받은 계정명이다
GRANT ALL PRIVILEGES ON `{username}`.* TO '{username}'@'localhost';
FLUSH PRIVILEGES;
'''
try:
subprocess.run(['sudo', 'mysql', '-u', 'root', f'-p{DB비번}'], input=db_sql, text=True)
print("
except Exception as e:
print(f"
# DNS zone 등록 // 자동으로 사용자계정명으로 서브 도메인을 Zone파일에 넣어준다
zone_file = "/etc/bind/db.wujin.monster"
subdomain_entry = f"\n{username}\tIN\tA\t183.103.115.250\n"
//subdomain_entry = f"\n{username}\tIN\tA\t183.103.115.250\n" 여기서 f" = f-string은 파이썬 3.6이상에서 도입된 문자열 포매팅 방식이다.
f로 시작되는 문자열 안에 중괄호로 {}감싸진 부분은 변수나 표현식의 값을 문자열에 삽입한다.
즉 중요한 {username}값을 코드에서 이미 정의된 username 변수의 값으로 치환된다. 즉 대체된다는 뜻 !
결국 이거지 {username} 이 {test}라고 가정하면 test.wujin.monster 서버외부IP 이렇게 서브도메인이 값이 들어간다는 것!
try:
with open(zone_file, "a") as f:
f.write(subdomain_entry)
except Exception as e:
print(f"존 파일 수정 에러: {e}")
subprocess.run(['sudo', 'systemctl', 'reload', 'bind9'])
# Apache 가상호스트 설정
apache_conf = f""" // f""" f-string은 파이썬 3.6이상에서 도입된 멀티라인 문자열 정의를 의미한다. 핵심은 문자열 내의 중괄호 {}를 이용해 변수의 값을
직접 삽입할 수 있다는 점이다 !
<VirtualHost *:80>
ServerName {username}.wujin.monster //여기서도 나왔지 중괄호 안에 유저네임 << 이 유저네임이 최초로 입력받은 사용자 계정ID랑 동일하다는것
DocumentRoot /home/{username}/public_html //도큐먼트루트의 경로를 아팟치는 어디든 리디렉션 해줄수 있어 결국 /var/www/html 안에
// 안 넣어도 된다는 거야 다른 경로도 얼마든지 할 수 있음
<Directory /home/{username}/public_html>
Options Indexes FollowSymLinks
AllowOverride All //아팟치 설정보면 이거 엄청 나오더라 .htaccess 파일을 허용할지 deny할지의 여부 리디렉션 or Url 재작성 or 접근 제어 or 보안 설정
Require all granted //mod_authz_core 아팟치 모듈인데 모든 사용자에게 허용한다는 의미 아팟치하면 또 모듈이지 개많아
</Directory>
ErrorLog /var/log/apache2/{username}_error.log
CustomLog /var/log/apache2/{username}_access.log combined
</VirtualHost>
"""
conf_path = f"/etc/apache2/sites-available/{username}.wujin.monster.conf"
// sites-available 여기안에 가상호스트 설정파일들이 즐비한곳 각각의 도메인들에 대한 설정이 포함된 곳
try:
with open(conf_path, "w") as f:
f.write(apache_conf)
subprocess.run(['sudo', 'a2ensite', f'{username}.wujin.monster.conf']) //모든 리눅스들은 일시적으로 root의 권한을 잠깐잠깐식 빌려서 명령을 처리한다
//'sudo', 'a2ensite', = 일시적으로 1행엠
subprocess.run(['sudo', 'systemctl', 'reload', 'apache2'])
except Exception as e:
print(f"Apache 설정 에러: {e}")
#
if board_type == "wordpress":
wp_zip_path = "/var/www/web_board/wordpress.zip"
with zipfile.ZipFile(wp_zip_path, "r") as zip_ref:
zip_ref.extractall(web_dir)
wp_subdir = os.path.join(web_dir, "wordpress")
if os.path.isdir(wp_subdir):
for f in os.listdir(wp_subdir):
shutil.move(os.path.join(wp_subdir, f), web_dir)
shutil.rmtree(wp_subdir)
print("
elif board_type == "phpbb3":
phpbb_zip_path = "/var/www/web_board/phpbb3.zip"
with zipfile.ZipFile(phpbb_zip_path, "r") as zip_ref:
zip_ref.extractall(web_dir)
phpbb_subdir = os.path.join(web_dir, "phpBB3")
if os.path.isdir(phpbb_subdir):
for f in os.listdir(phpbb_subdir):
shutil.move(os.path.join(phpbb_subdir, f), web_dir)
shutil.rmtree(phpbb_subdir)
#
permission_paths = [
"cache", "store", "files", "images/avatars/upload"
]
for p in permission_paths:
target = os.path.join(web_dir, p)
if os.path.exists(target):
subprocess.run(['sudo', 'chmod', '-R', '777', target])
subprocess.run(['sudo', 'chown', '-R', f'{username}:{username}', target])
#
config_file = os.path.join(web_dir, "config.php")
if os.path.exists(config_file):
subprocess.run(['sudo', 'chmod', '666', config_file])
subprocess.run(['sudo', 'chown', f'{username}:{username}', config_file])
print("
elif board_type == "gnuboard5":
gnu_zip_path = "/var/www/web_board/gnuboard5.zip"
with zipfile.ZipFile(gnu_zip_path, "r") as zip_ref:
zip_ref.extractall(web_dir)
# 압축 풀었을 때 생성된 디렉토리 이름을 자동 추적
extracted_dirs = [d for d in os.listdir(web_dir) if os.path.isdir(os.path.join(web_dir, d))]
if len(extracted_dirs) == 1:
gnu_subdir = os.path.join(web_dir, extracted_dirs[0])
for f in os.listdir(gnu_subdir):
shutil.move(os.path.join(gnu_subdir, f), web_dir)
shutil.rmtree(gnu_subdir)
#
data_dir = os.path.join(web_dir, "data")
os.makedirs(data_dir, exist_ok=True)
subprocess.run(['sudo', 'chmod', '707', data_dir])
subprocess.run(['sudo', 'chown', f'{username}:{username}', data_dir])
print("
# 퍼미션 정리
subprocess.run(['sudo', 'chown', '-R', f'{username}:{username}', web_dir])
# 완료 출력
print("\n===============================")
print(f"[완료] {username} 호스팅 계정 생성 완료!")
print(f"
print("===============================")