Initial commit: web-hosts infrastructure

- hostctl.sh management script for starting/stopping/restarting hosts
- test.coppertone.tech domain setup with compose.yaml
- deploy.sh for automated deployments from testing branch
- frontend-nginx.conf for static file serving

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Administrator
2025-11-23 19:54:01 +01:00
parent 57669db6ac
commit 69a28b0537
9 changed files with 1006 additions and 55 deletions

83
.gitignore vendored
View File

@@ -79,6 +79,8 @@ web_modules/
.env.test.local .env.test.local
.env.production.local .env.production.local
.env.local .env.local
*.env.local
*.env.production
# parcel-bundler cache (https://parceljs.org/) # parcel-bundler cache (https://parceljs.org/)
.cache .cache
@@ -94,16 +96,12 @@ dist
# Gatsby files # Gatsby files
.cache/ .cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output # vuepress build output
.vuepress/dist .vuepress/dist
# vuepress v2.x temp and cache directory # vuepress v2.x temp and cache directory
.temp .temp
.cache
# vitepress build output # vitepress build output
**/.vitepress/dist **/.vitepress/dist
@@ -137,9 +135,6 @@ dist
.pnp.* .pnp.*
# ---> Go # ---> Go
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins # Binaries for programs and plugins
*.exe *.exe
*.exe~ *.exe~
@@ -153,19 +148,12 @@ dist
# Output of the go coverage tool, specifically when used with LiteIDE # Output of the go coverage tool, specifically when used with LiteIDE
*.out *.out
# Dependency directories (remove the comment below to include it)
# vendor/
# Go workspace file # Go workspace file
go.work go.work
go.work.sum go.work.sum
# env file
.env
# ---> Rust # ---> Rust
# Generated by Cargo # Generated by Cargo
# will have compiled files and executables
debug/ debug/
target/ target/
@@ -175,12 +163,6 @@ target/
# MSVC Windows builds of rustc generate these, which store debugging information # MSVC Windows builds of rustc generate these, which store debugging information
*.pdb *.pdb
# RustRover
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# ---> C++ # ---> C++
# Prerequisites # Prerequisites
*.d *.d
@@ -195,11 +177,6 @@ target/
*.gch *.gch
*.pch *.pch
# Compiled Dynamic libraries
*.so
*.dylib
*.dll
# Fortran module files # Fortran module files
*.mod *.mod
*.smod *.smod
@@ -211,18 +188,11 @@ target/
*.lib *.lib
# Executables # Executables
*.exe
*.out
*.app *.app
# ---> C # ---> C
# Prerequisites
*.d
# Object files # Object files
*.o
*.ko *.ko
*.obj
*.elf *.elf
# Linker output # Linker output
@@ -230,38 +200,15 @@ target/
*.map *.map
*.exp *.exp
# Precompiled Headers
*.gch
*.pch
# Libraries
*.lib
*.a
*.la
*.lo
# Shared objects (inc. Windows DLLs) # Shared objects (inc. Windows DLLs)
*.dll
*.so
*.so.* *.so.*
*.dylib
# Executables
*.exe
*.out
*.app
*.i*86
*.x86_64
*.hex
# Debug files # Debug files
*.dSYM/ *.dSYM/
*.su *.su
*.idb *.idb
*.pdb
# Kernel Module Compile Results # Kernel Module Compile Results
*.mod*
*.cmd *.cmd
.tmp_versions/ .tmp_versions/
modules.order modules.order
@@ -283,3 +230,29 @@ CTestTestfile.cmake
_deps _deps
CMakeUserPresets.json CMakeUserPresets.json
# ---> Web Hosts Specific
# Data directories
**/data/
**/postgres-data/
# Repo clones (managed separately)
**/repo/
# Runtime files
*.sock
# OS files
.DS_Store
Thumbs.db
# Editor files
*.swp
*.swo
*~
.idea/
.vscode/
# Temporary files
tmp/
temp/
*.tmp

View File

@@ -0,0 +1,41 @@
# test.coppertone.tech
## Directory Structure
```
test.coppertone.tech/
├── compose.yaml # Podman compose configuration
├── www/ # Static website files
├── data/ # Persistent data volumes
├── config/ # Configuration files
└── README.md # This file
```
## Port Assignment
- Main site (test.coppertone.tech): `127.0.0.1:9100`
- API (api.test.coppertone.tech): `127.0.0.1:9101` (if enabled)
## Commands
```bash
# Start the domain
hostctl start test.coppertone.tech
# Stop the domain
hostctl stop test.coppertone.tech
# View logs
hostctl logs test.coppertone.tech
# Check status
hostctl status test.coppertone.tech
```
## Nginx Configuration
After starting the containers, add nginx configuration:
```bash
sudo /docker/www/startup.sh add-domain test.coppertone.tech 9100
```

View File

@@ -0,0 +1,99 @@
# Compose file for test.coppertone.tech
# Builds from CopperTone.Tech repo (develop branch)
# All services bind to 127.0.0.1 only (routed via nginx reverse proxy)
#
# Deploy with: ./deploy.sh
# Or manually: podman-compose down && podman-compose build && podman-compose up -d
services:
# Frontend from develop branch
frontend:
build:
context: ./repo/frontend
dockerfile: Containerfile
container_name: test-coppertone-tech-frontend
restart: unless-stopped
ports:
- "127.0.0.1:9100:80"
volumes:
- ./frontend-nginx.conf:/etc/nginx/conf.d/default.conf:ro
environment:
- VITE_API_BASE_URL=https://test.coppertone.tech/api
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost/"]
interval: 30s
timeout: 10s
retries: 3
# Auth service
auth-service:
build:
context: ./repo/backend/functions/auth-service
dockerfile: Containerfile
container_name: test-coppertone-tech-auth
restart: unless-stopped
ports:
- "127.0.0.1:9102:8080"
environment:
JWT_SECRET: ${JWT_SECRET:-test_jwt_secret_key_change_me_in_production_at_least_64_characters_long}
DEFAULT_USER_ROLE: ${DEFAULT_USER_ROLE:-CLIENT}
DB_HOST: db
DB_USER: ${DB_USER:-testuser}
DB_PASSWORD: ${DB_PASSWORD:-testpassword}
DB_NAME: ${DB_NAME:-testcoppertone_db}
DB_SSL_MODE: ${DB_SSL_MODE:-disable}
CORS_ALLOW_ORIGIN: https://test.coppertone.tech
depends_on:
- db
# Work management service
work-management-service:
build:
context: ./repo/backend/functions/work-management-service
dockerfile: Containerfile
container_name: test-coppertone-tech-work-mgmt
restart: unless-stopped
ports:
- "127.0.0.1:9103:8080"
environment:
JWT_SECRET: ${JWT_SECRET:-test_jwt_secret_key_change_me_in_production_at_least_64_characters_long}
DB_HOST: db
DB_USER: ${DB_USER:-testuser}
DB_PASSWORD: ${DB_PASSWORD:-testpassword}
DB_NAME: ${DB_NAME:-testcoppertone_db}
DB_SSL_MODE: ${DB_SSL_MODE:-disable}
CORS_ALLOW_ORIGIN: https://test.coppertone.tech
depends_on:
- db
# Blog service
blog-service:
build:
context: ./repo/backend/functions/blog-service
dockerfile: Containerfile
container_name: test-coppertone-tech-blog
restart: unless-stopped
ports:
- "127.0.0.1:9105:8080"
environment:
JWT_SECRET: ${JWT_SECRET:-test_jwt_secret_key_change_me_in_production_at_least_64_characters_long}
DB_HOST: db
DB_USER: ${DB_USER:-testuser}
DB_PASSWORD: ${DB_PASSWORD:-testpassword}
DB_NAME: ${DB_NAME:-testcoppertone_db}
DB_SSL_MODE: ${DB_SSL_MODE:-disable}
CORS_ALLOW_ORIGIN: https://test.coppertone.tech
depends_on:
- db
# Database for test environment
db:
image: postgres:16-alpine
container_name: test-coppertone-tech-db
restart: unless-stopped
environment:
POSTGRES_DB: testcoppertone_db
POSTGRES_USER: testuser
POSTGRES_PASSWORD: testpassword
volumes:
- ./data/postgres:/var/lib/postgresql/data

View File

@@ -0,0 +1,139 @@
#!/bin/bash
#
# Deploy script for test.coppertone.tech
# Pulls latest from develop branch, rebuilds, and restarts containers
#
# Usage:
# ./deploy.sh # Full deploy (pull, build, restart)
# ./deploy.sh pull # Just pull latest code
# ./deploy.sh build # Just rebuild containers
# ./deploy.sh restart # Just restart containers
# ./deploy.sh status # Show status
# ./deploy.sh logs # Show logs
#
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_DIR="$SCRIPT_DIR/repo"
BRANCH="${DEPLOY_BRANCH:-testing}"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
log_success() { echo -e "${GREEN}[OK]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
cd "$SCRIPT_DIR"
pull_latest() {
log_info "Pulling latest from $BRANCH branch..."
cd "$REPO_DIR"
# Ensure we're on the right branch
current_branch=$(git branch --show-current)
if [ "$current_branch" != "$BRANCH" ]; then
log_info "Switching from $current_branch to $BRANCH..."
git checkout "$BRANCH"
fi
# Pull latest
git fetch origin "$BRANCH"
git reset --hard "origin/$BRANCH"
# Show latest commit
log_success "Updated to: $(git log -1 --oneline)"
cd "$SCRIPT_DIR"
}
build_containers() {
log_info "Building containers..."
podman-compose build --no-cache
log_success "Build complete"
}
stop_containers() {
log_info "Stopping containers..."
podman-compose down 2>/dev/null || true
log_success "Containers stopped"
}
start_containers() {
log_info "Starting containers..."
podman-compose up -d
log_success "Containers started"
}
show_status() {
log_info "Container status:"
podman-compose ps
echo ""
log_info "Recent commits:"
cd "$REPO_DIR"
git log -3 --oneline
cd "$SCRIPT_DIR"
}
show_logs() {
podman-compose logs -f --tail 50
}
full_deploy() {
echo "=========================================="
echo " Deploying test.coppertone.tech"
echo " Branch: $BRANCH"
echo "=========================================="
echo ""
pull_latest
echo ""
stop_containers
echo ""
build_containers
echo ""
start_containers
echo ""
log_success "=========================================="
log_success " Deployment complete!"
log_success "=========================================="
echo ""
show_status
}
case "${1:-deploy}" in
deploy|"")
full_deploy
;;
pull)
pull_latest
;;
build)
build_containers
;;
restart)
stop_containers
start_containers
;;
stop)
stop_containers
;;
start)
start_containers
;;
status)
show_status
;;
logs)
show_logs
;;
*)
echo "Usage: $0 {deploy|pull|build|restart|stop|start|status|logs}"
exit 1
;;
esac

View File

@@ -0,0 +1,44 @@
# Simplified nginx config for test.coppertone.tech frontend
# API routing is handled by the external nginx reverse proxy
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html index.htm;
# Security Headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
# Prevent caching of index.html
location = /index.html {
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Expires "0";
}
# Cache static assets
location /assets/ {
add_header Cache-Control "public, max-age=31536000, immutable";
}
# Serve markdown files
location ~* \.md$ {
types { text/plain md; }
add_header Content-Type text/plain;
}
# SPA routing - serve index.html for all non-file requests
location / {
try_files $uri $uri/ /index.html;
}
# Health check
location /health {
return 200 "OK";
add_header Content-Type text/plain;
}
}

View File

@@ -0,0 +1,32 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Welcome to test.coppertone.tech</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.container {
text-align: center;
padding: 2rem;
}
h1 { font-size: 3rem; margin-bottom: 0.5rem; }
p { font-size: 1.2rem; opacity: 0.9; }
</style>
</head>
<body>
<div class="container">
<h1>test.coppertone.tech</h1>
<p>Your site is ready to be configured.</p>
</div>
</body>
</html>

124
scripts/README.md Normal file
View File

@@ -0,0 +1,124 @@
# Web Hosts Management
## Overview
This system manages web hosting for multiple domains using:
- **Podman Compose** - Container orchestration (rootless)
- **Nginx** - Reverse proxy with SSL (rootful, via sudo)
- All services bind to `127.0.0.1` only
## Directory Structure
```
/docker/web-hosts/
├── scripts/
│ └── hostctl.sh # Main management script
├── domains/
│ └── <domain>/ # One directory per root domain
│ ├── compose.yaml
│ ├── www/ # Static files
│ ├── data/ # Persistent data
│ └── config/ # Configuration
```
## Quick Start
### 1. Create a new domain
```bash
/docker/web-hosts/scripts/hostctl.sh create example.com 9000
```
This creates:
- Directory structure at `/docker/web-hosts/domains/example.com/`
- Template `compose.yaml` with services bound to `127.0.0.1:9000`
- Default `index.html`
### 2. Customize the compose file
Edit `/docker/web-hosts/domains/example.com/compose.yaml` to add your services.
### 3. Start the domain
```bash
/docker/web-hosts/scripts/hostctl.sh start example.com
```
### 4. Add nginx reverse proxy with SSL
```bash
sudo /docker/www/startup.sh add-domain example.com 9000
```
## Commands Reference
| Command | Description |
|---------|-------------|
| `hostctl list` | List all configured domains |
| `hostctl status [domain]` | Show status |
| `hostctl start <domain>` | Start a domain |
| `hostctl stop <domain>` | Stop a domain |
| `hostctl restart <domain>` | Restart a domain |
| `hostctl logs <domain>` | View logs |
| `hostctl create <domain> [port]` | Create new domain |
| `hostctl remove <domain>` | Remove a domain |
| `hostctl start-all` | Start all domains |
| `hostctl stop-all` | Stop all domains |
## Port Assignment Convention
Each root domain gets a port range starting from the specified port:
- `<port>`: Main website (www)
- `<port+1>`: API subdomain
- `<port+2>`: Additional services
- etc.
Example for `example.com` starting at port 9000:
- `example.com``127.0.0.1:9000`
- `api.example.com``127.0.0.1:9001`
- `admin.example.com``127.0.0.1:9002`
## Architecture
```
Internet
┌─────────────────────────────────────┐
│ Nginx (rootful, ports 80/443) │
│ /docker/www/ │
│ - SSL termination │
│ - Reverse proxy │
└─────────────────────────────────────┘
▼ 127.0.0.1:<port>
┌─────────────────────────────────────┐
│ Podman Containers (rootless) │
│ /docker/web-hosts/domains/ │
│ - Web apps │
│ - APIs │
│ - Databases │
└─────────────────────────────────────┘
```
## Adding Subdomains
1. Add service to domain's `compose.yaml`:
```yaml
services:
api:
image: your-api-image
ports:
- "127.0.0.1:9001:8080"
```
2. Restart the domain:
```bash
hostctl restart example.com
```
3. Add nginx config for subdomain:
```bash
sudo /docker/www/startup.sh add-domain api.example.com 9001
```

1
scripts/hostctl Symbolic link
View File

@@ -0,0 +1 @@
/docker/web-hosts/scripts/hostctl.sh

498
scripts/hostctl.sh Executable file
View File

@@ -0,0 +1,498 @@
#!/bin/bash
#
# Web Host Management Script
# Manages podman-compose based web hosts on 127.0.0.1
# Each root domain has its own compose file in /docker/web-hosts/domains/<domain>/
#
set -e
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m'
# Configuration
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
HOSTS_DIR="$(dirname "$SCRIPT_DIR")"
DOMAINS_DIR="$HOSTS_DIR/domains"
NGINX_STARTUP="/docker/www/startup.sh"
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
log_success() { echo -e "${GREEN}[OK]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
log_header() { echo -e "${CYAN}=== $1 ===${NC}"; }
# Get domain directory
get_domain_dir() {
local domain=$1
echo "$DOMAINS_DIR/$domain"
}
# Check if domain exists
domain_exists() {
local domain=$1
local domain_dir=$(get_domain_dir "$domain")
[ -d "$domain_dir" ] && [ -f "$domain_dir/compose.yaml" ]
}
# List all domains
list_domains() {
log_header "Configured Domains"
echo ""
if [ ! -d "$DOMAINS_DIR" ] || [ -z "$(ls -A "$DOMAINS_DIR" 2>/dev/null)" ]; then
log_warn "No domains configured yet."
echo ""
echo "To create a new domain, run:"
echo " $0 create <domain>"
return
fi
for domain_dir in "$DOMAINS_DIR"/*/; do
if [ -d "$domain_dir" ]; then
domain=$(basename "$domain_dir")
compose_file="$domain_dir/compose.yaml"
if [ -f "$compose_file" ]; then
# Check running status
cd "$domain_dir"
running_count=$(podman-compose ps -q 2>/dev/null | wc -l)
if [ "$running_count" -gt 0 ]; then
status="${GREEN}RUNNING${NC} ($running_count containers)"
else
status="${YELLOW}STOPPED${NC}"
fi
echo -e " $domain: $status"
# Show services
if [ -f "$compose_file" ]; then
services=$(grep -E "^\s+[a-zA-Z0-9_-]+:\s*$" "$compose_file" | sed 's/://g' | xargs)
if [ -n "$services" ]; then
echo " Services: $services"
fi
fi
echo ""
fi
fi
done
}
# Start a domain
start_domain() {
local domain=$1
if [ -z "$domain" ]; then
log_error "Usage: $0 start <domain>"
return 1
fi
if ! domain_exists "$domain"; then
log_error "Domain '$domain' not found. Create it first with: $0 create $domain"
return 1
fi
local domain_dir=$(get_domain_dir "$domain")
log_info "Starting $domain..."
cd "$domain_dir"
podman-compose up -d
log_success "$domain started"
# Show running containers
podman-compose ps
}
# Stop a domain
stop_domain() {
local domain=$1
if [ -z "$domain" ]; then
log_error "Usage: $0 stop <domain>"
return 1
fi
if ! domain_exists "$domain"; then
log_error "Domain '$domain' not found."
return 1
fi
local domain_dir=$(get_domain_dir "$domain")
log_info "Stopping $domain..."
cd "$domain_dir"
podman-compose down
log_success "$domain stopped"
}
# Restart a domain
restart_domain() {
local domain=$1
if [ -z "$domain" ]; then
log_error "Usage: $0 restart <domain>"
return 1
fi
stop_domain "$domain"
sleep 1
start_domain "$domain"
}
# Show domain status
status_domain() {
local domain=$1
if [ -z "$domain" ]; then
# Show all domains
list_domains
return
fi
if ! domain_exists "$domain"; then
log_error "Domain '$domain' not found."
return 1
fi
local domain_dir=$(get_domain_dir "$domain")
log_header "Status: $domain"
cd "$domain_dir"
podman-compose ps
}
# Show domain logs
logs_domain() {
local domain=$1
local service=$2
if [ -z "$domain" ]; then
log_error "Usage: $0 logs <domain> [service]"
return 1
fi
if ! domain_exists "$domain"; then
log_error "Domain '$domain' not found."
return 1
fi
local domain_dir=$(get_domain_dir "$domain")
cd "$domain_dir"
if [ -n "$service" ]; then
podman-compose logs -f "$service"
else
podman-compose logs -f
fi
}
# Create a new domain
create_domain() {
local domain=$1
local port=$2
if [ -z "$domain" ]; then
log_error "Usage: $0 create <domain> [starting_port]"
log_error "Example: $0 create example.com 9000"
return 1
fi
# Default starting port
port=${port:-9000}
local domain_dir=$(get_domain_dir "$domain")
if [ -d "$domain_dir" ]; then
log_warn "Domain '$domain' already exists at $domain_dir"
return 1
fi
log_info "Creating domain: $domain"
# Create directory structure
mkdir -p "$domain_dir"/{www,data,config}
# Create compose.yaml template
cat > "$domain_dir/compose.yaml" << EOF
# Compose file for $domain
# All services bind to 127.0.0.1 only (routed via nginx reverse proxy)
#
# Port assignments for $domain:
# - Main site ($domain): $port
# - API (api.$domain): $((port + 1))
# - Add more subdomains as needed
#
services:
# Main website
www:
image: docker.io/nginx:stable-alpine
container_name: ${domain//./-}-www
restart: unless-stopped
ports:
- "127.0.0.1:${port}:80"
volumes:
- ./www:/usr/share/nginx/html:ro
# healthcheck:
# test: ["CMD", "curl", "-f", "http://localhost/"]
# interval: 30s
# timeout: 10s
# retries: 3
# Uncomment to add API subdomain
# api:
# image: your-api-image
# container_name: ${domain//./-}-api
# restart: unless-stopped
# ports:
# - "127.0.0.1:$((port + 1)):8080"
# volumes:
# - ./data:/app/data
# environment:
# - NODE_ENV=production
# Shared network (optional)
# networks:
# default:
# name: ${domain//./-}-network
EOF
# Create default index.html
cat > "$domain_dir/www/index.html" << EOF
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Welcome to $domain</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.container {
text-align: center;
padding: 2rem;
}
h1 { font-size: 3rem; margin-bottom: 0.5rem; }
p { font-size: 1.2rem; opacity: 0.9; }
</style>
</head>
<body>
<div class="container">
<h1>$domain</h1>
<p>Your site is ready to be configured.</p>
</div>
</body>
</html>
EOF
# Create README
cat > "$domain_dir/README.md" << EOF
# $domain
## Directory Structure
\`\`\`
$domain/
├── compose.yaml # Podman compose configuration
├── www/ # Static website files
├── data/ # Persistent data volumes
├── config/ # Configuration files
└── README.md # This file
\`\`\`
## Port Assignment
- Main site ($domain): \`127.0.0.1:$port\`
- API (api.$domain): \`127.0.0.1:$((port + 1))\` (if enabled)
## Commands
\`\`\`bash
# Start the domain
hostctl start $domain
# Stop the domain
hostctl stop $domain
# View logs
hostctl logs $domain
# Check status
hostctl status $domain
\`\`\`
## Nginx Configuration
After starting the containers, add nginx configuration:
\`\`\`bash
sudo /docker/www/startup.sh add-domain $domain $port
\`\`\`
EOF
log_success "Domain created: $domain_dir"
echo ""
echo "Next steps:"
echo " 1. Edit $domain_dir/compose.yaml to configure services"
echo " 2. Add your website files to $domain_dir/www/"
echo " 3. Start with: $0 start $domain"
echo " 4. Add nginx config: sudo /docker/www/startup.sh add-domain $domain $port"
}
# Remove a domain
remove_domain() {
local domain=$1
local force=$2
if [ -z "$domain" ]; then
log_error "Usage: $0 remove <domain> [--force]"
return 1
fi
if ! domain_exists "$domain"; then
log_error "Domain '$domain' not found."
return 1
fi
local domain_dir=$(get_domain_dir "$domain")
# Check if running
cd "$domain_dir"
if [ "$(podman-compose ps -q 2>/dev/null | wc -l)" -gt 0 ]; then
if [ "$force" != "--force" ]; then
log_error "Domain '$domain' is running. Stop it first or use --force"
return 1
fi
log_warn "Force stopping $domain..."
podman-compose down
fi
if [ "$force" != "--force" ]; then
echo -n "Are you sure you want to remove $domain? This will delete all files! (y/N) "
read -r confirm
if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
log_info "Cancelled."
return 0
fi
fi
rm -rf "$domain_dir"
log_success "Domain '$domain' removed."
}
# Start all domains
start_all() {
log_header "Starting All Domains"
for domain_dir in "$DOMAINS_DIR"/*/; do
if [ -d "$domain_dir" ] && [ -f "$domain_dir/compose.yaml" ]; then
domain=$(basename "$domain_dir")
start_domain "$domain"
echo ""
fi
done
log_success "All domains started"
}
# Stop all domains
stop_all() {
log_header "Stopping All Domains"
for domain_dir in "$DOMAINS_DIR"/*/; do
if [ -d "$domain_dir" ] && [ -f "$domain_dir/compose.yaml" ]; then
domain=$(basename "$domain_dir")
stop_domain "$domain"
echo ""
fi
done
log_success "All domains stopped"
}
# Show usage
usage() {
echo "Web Host Management Script"
echo ""
echo "Usage: $0 <command> [options]"
echo ""
echo "Commands:"
echo " list List all configured domains"
echo " status [domain] Show status (all or specific domain)"
echo " start <domain> Start a domain"
echo " stop <domain> Stop a domain"
echo " restart <domain> Restart a domain"
echo " logs <domain> [svc] View logs for a domain"
echo ""
echo " create <domain> [port] Create a new domain (default port: 9000)"
echo " remove <domain> Remove a domain"
echo ""
echo " start-all Start all domains"
echo " stop-all Stop all domains"
echo ""
echo "Examples:"
echo " $0 create example.com 9000"
echo " $0 start example.com"
echo " $0 logs example.com www"
echo " $0 status"
echo ""
echo "Directory structure:"
echo " $DOMAINS_DIR/<domain>/compose.yaml"
}
# Main entry point
case "${1:-help}" in
list)
list_domains
;;
status)
status_domain "$2"
;;
start)
start_domain "$2"
;;
stop)
stop_domain "$2"
;;
restart)
restart_domain "$2"
;;
logs)
logs_domain "$2" "$3"
;;
create)
create_domain "$2" "$3"
;;
remove|delete)
remove_domain "$2" "$3"
;;
start-all)
start_all
;;
stop-all)
stop_all
;;
help|--help|-h|"")
usage
;;
*)
log_error "Unknown command: $1"
usage
exit 1
;;
esac