feat(PROJ-27): Docker-Support + install.sh v2.0 (native + Docker-Modus)

Adds multi-stage Dockerfiles (Go+Xapian CGO, Next.js standalone),
docker-compose.yml, webhook-basierter Deploy-Flow und erweitertes
install.sh mit interaktiver Modus-Auswahl.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
sysops
2026-03-31 10:18:11 +02:00
parent 842640c3aa
commit 5f0c7a7e6d
9 changed files with 814 additions and 224 deletions
+30
View File
@@ -0,0 +1,30 @@
# Git
.git
.gitignore
# Node
node_modules
.next
npm-debug.log*
# Go build artifacts
archivmail
archivmail-export
archivmail-import
# Dev/Test
*.test
config.test.yml
.env.local
.env.local.example
# Secrets (nie ins Image)
keyfile
*.key
*.pem
# Docs / IDE
docs/
.claude/
.vscode/
*.md
+37
View File
@@ -0,0 +1,37 @@
# ── Stage 1: Go Backend Build (CGO + Xapian) ─────────────────────────────────
FROM golang:1.24-bookworm AS go-builder
RUN apt-get update && apt-get install -y --no-install-recommends \
libxapian-dev pkg-config build-essential \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /build
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=1 go build -tags xapian -buildvcs=false \
-ldflags="-s -w" \
-o /out/archivmail ./cmd/archivmail/
# ── Stage 2: Runtime Image ────────────────────────────────────────────────────
FROM debian:bookworm-slim
# Xapian runtime library only (no dev headers)
RUN apt-get update && apt-get install -y --no-install-recommends \
libxapian30 ca-certificates \
&& rm -rf /var/lib/apt/lists/*
RUN useradd --system --shell /bin/false --uid 999 --create-home archivmail
COPY --from=go-builder /out/archivmail /usr/local/bin/archivmail
# Volumes: config + data werden von außen gemountet
RUN mkdir -p /var/archivmail/store /var/archivmail/astore /var/archivmail/xapian \
&& chown -R archivmail:archivmail /var/archivmail
USER archivmail
EXPOSE 8080 2525 1143
ENTRYPOINT ["/usr/local/bin/archivmail"]
CMD ["-config", "/etc/archivmail/config.yml"]
+31
View File
@@ -0,0 +1,31 @@
# ── Stage 1: Next.js Build ────────────────────────────────────────────────────
FROM node:22-bookworm-slim AS node-builder
WORKDIR /build
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
# ── Stage 2: Runtime (Next.js standalone) ─────────────────────────────────────
FROM node:22-bookworm-slim
WORKDIR /app
RUN useradd --system --shell /bin/false --uid 998 --create-home nextjs
COPY --from=node-builder /build/.next/standalone ./
COPY --from=node-builder /build/.next/static ./.next/static
COPY --from=node-builder /build/public ./public
RUN chown -R nextjs:nextjs /app
USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME=0.0.0.0
# NEXT_PUBLIC_API_URL wird zur Laufzeit gesetzt (docker-compose environment)
CMD ["node", "server.js"]
+57
View File
@@ -0,0 +1,57 @@
# archivmail Docker-Konfiguration
# Kopieren nach: /etc/archivmail/config.yml
# Wird als Read-Only-Volume in den Container gemountet.
server:
api_port: 8080
smtp_port: 2525
database:
host: postgres # Docker Compose Service-Name (nicht localhost!)
port: 5432
name: archivmail
user: archivmail
password: CHANGE_ME_DB_PASSWORD
sslmode: disable
storage:
store_path: /var/archivmail/store
astore_path: /var/archivmail/astore
xapian_path: /var/archivmail/xapian
keyfile: /etc/archivmail/keyfile
index:
path: /var/archivmail/xapian
backend: xapian
batch_size: 100
async_queue_size: 1000
api:
bind: "0.0.0.0:8080"
secret: CHANGE_ME_64_CHAR_SECRET
secure_cookies: true
trusted_proxies:
- "172.16.0.0/12" # Docker-interne Netzwerke
- "127.0.0.1"
smtp:
enabled: true
bind: "0.0.0.0:2525"
domain: mail.example.com
max_size_mb: 25
allowed_ips:
- "0.0.0.0/0"
- "::/0"
tenant_routing: default
default_tenant_id: 1
imap_server:
enabled: false
bind: "0.0.0.0:1143"
audit:
log_path: /var/archivmail/audit.log
retention_days: 365
logging:
level: info
+54
View File
@@ -0,0 +1,54 @@
services:
# ── PostgreSQL ──────────────────────────────────────────────────────────────
postgres:
image: postgres:16-bookworm
restart: unless-stopped
environment:
POSTGRES_DB: archivmail
POSTGRES_USER: archivmail
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U archivmail"]
interval: 10s
timeout: 5s
retries: 5
# ── Go Backend ──────────────────────────────────────────────────────────────
archivmail:
build:
context: .
dockerfile: Dockerfile
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
volumes:
- /etc/archivmail:/etc/archivmail:ro
- archivmail_data:/var/archivmail
ports:
- "127.0.0.1:8080:8080" # API (nginx proxied)
- "2525:2525" # SMTP BCC
- "1143:1143" # IMAP (nginx proxied für TLS)
environment:
- TZ=Europe/Berlin
# ── Next.js Frontend ────────────────────────────────────────────────────────
archivmail-web:
build:
context: .
dockerfile: Dockerfile.web
restart: unless-stopped
depends_on:
- archivmail
ports:
- "127.0.0.1:3000:3000" # Frontend (nginx proxied)
environment:
- NEXT_PUBLIC_API_URL=http://archivmail:8080
- TZ=Europe/Berlin
volumes:
postgres_data:
archivmail_data:
+4 -4
View File
@@ -42,14 +42,14 @@
| PROJ-26 | IMAP-Server-Schnittstelle (Read-Only Archivzugriff) | In Progress | [PROJ-26](PROJ-26-imap-server-schnittstelle.md) | 2026-03-18 | | PROJ-26 | IMAP-Server-Schnittstelle (Read-Only Archivzugriff) | In Progress | [PROJ-26](PROJ-26-imap-server-schnittstelle.md) | 2026-03-18 |
| PROJ-27 | Container-Ready (Dockerfile + Env-Vars) | In Review | [PROJ-27](PROJ-27-container-ready.md) | 2026-03-28 | | PROJ-27 | Container-Ready (Dockerfile + Env-Vars) | In Review | [PROJ-27](PROJ-27-container-ready.md) | 2026-03-28 |
| PROJ-28 | Self-Service Onboarding (Sign-up, E-Mail-Verifikation, Passwort-Reset) | Planned | [PROJ-28](PROJ-28-self-service-onboarding.md) | 2026-03-28 | | PROJ-28 | Self-Service Onboarding (Sign-up, E-Mail-Verifikation, Passwort-Reset) | In Progress | [PROJ-28](PROJ-28-self-service-onboarding.md) | 2026-03-28 |
| PROJ-29 | Tenant-Quotas & Usage-Limits | Planned | [PROJ-29](PROJ-29-tenant-quotas.md) | 2026-03-28 | | PROJ-29 | Tenant-Quotas & Usage-Limits | Planned | [PROJ-29](PROJ-29-tenant-quotas.md) | 2026-03-28 |
| PROJ-30 | Volltext-Index: Xapian → Manticore Search Migration | Planned | [PROJ-30](PROJ-30-bleve-migration.md) | 2026-03-28 | | PROJ-30 | Volltext-Index: Xapian → Manticore Search Migration | Planned | [PROJ-30](PROJ-30-bleve-migration.md) | 2026-03-28 |
| PROJ-31 | Billing & Subscriptions (Stripe) | Planned | [PROJ-31](PROJ-31-billing-subscriptions.md) | 2026-03-28 | | PROJ-31 | Billing & Subscriptions (Stripe) | Planned | [PROJ-31](PROJ-31-billing-subscriptions.md) | 2026-03-28 |
| PROJ-32 | Message-ID-basierte Duplikatserkennung | In Progress | [PROJ-32](PROJ-32-message-id-dedup.md) | 2026-03-31 | | PROJ-32 | Message-ID-basierte Duplikatserkennung | Deployed | [PROJ-32](PROJ-32-message-id-dedup.md) | 2026-03-31 |
| PROJ-33 | IMAP-Modus: Gemeinsames Archiv vs. Persönlicher Posteingang | Planned | [PROJ-33](PROJ-33-imap-modus-shared-personal.md) | 2026-03-31 | | PROJ-33 | IMAP-Modus: Gemeinsames Archiv vs. Persönlicher Posteingang | Deployed | [PROJ-33](PROJ-33-imap-modus-shared-personal.md) | 2026-03-31 |
| PROJ-34 | Retention-Policy + Löschsperre (GoBD-Compliance) | In Progress | [PROJ-34](PROJ-34-retention-policy.md) | 2026-03-31 | | PROJ-34 | Retention-Policy + Löschsperre (GoBD-Compliance) | Deployed | [PROJ-34](PROJ-34-retention-policy.md) | 2026-03-31 |
<!-- Add features above this line --> <!-- Add features above this line -->
+408 -90
View File
@@ -1,8 +1,11 @@
#!/bin/bash #!/bin/bash
# archivmail Server-Installer # archivmail Installer
# Unterstützte Systeme: Debian 12 / 13 # Unterstützte Systeme: Debian 12 / 13
# Aufruf: bash install.sh #
# Mit eigenem DB-Passwort: DB_PASSWORD=geheim bash install.sh # Aufruf:
# bash install.sh # Interaktiv (Modus-Auswahl)
# INSTALL_MODE=native bash install.sh # Nativ (systemd, direkter Build)
# INSTALL_MODE=docker bash install.sh # Docker (alle Abhängigkeiten im Container)
set -euo pipefail set -euo pipefail
export DEBIAN_FRONTEND=noninteractive export DEBIAN_FRONTEND=noninteractive
@@ -15,8 +18,11 @@ die() { echo -e "${RED}[ERR]${NC} $*" >&2; exit 1; }
[[ $EUID -eq 0 ]] || die "Bitte als root ausführen: sudo bash install.sh" [[ $EUID -eq 0 ]] || die "Bitte als root ausführen: sudo bash install.sh"
# ── Gemeinsame Variablen ──────────────────────────────────────────────────────
DB_PASSWORD="${DB_PASSWORD:-$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 32)}" DB_PASSWORD="${DB_PASSWORD:-$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 32)}"
API_SECRET="${API_SECRET:-$(openssl rand -base64 48 | tr -dc 'a-zA-Z0-9' | head -c 64)}" API_SECRET="${API_SECRET:-$(openssl rand -base64 48 | tr -dc 'a-zA-Z0-9' | head -c 64)}"
WEBHOOK_SECRET="${WEBHOOK_SECRET:-$(openssl rand -base64 32 | tr -dc 'a-zA-Z0-9' | head -c 40)}"
INSTALL_DIR="/opt/archivmail" INSTALL_DIR="/opt/archivmail"
STORE_DIR="/var/archivmail" STORE_DIR="/var/archivmail"
LOG_DIR="/var/log/archivmail" LOG_DIR="/var/log/archivmail"
@@ -24,17 +30,358 @@ CONFIG_DIR="/etc/archivmail"
SSL_DIR="/etc/ssl/archivmail" SSL_DIR="/etc/ssl/archivmail"
AM_USER="archivmail" AM_USER="archivmail"
FQDN="$(hostname -f 2>/dev/null || hostname)" FQDN="$(hostname -f 2>/dev/null || hostname)"
REPO_URL="${REPO_URL:-https://gitea.perlbach24.de/scripte/archivmail.git}" REPO_URL="${REPO_URL:-https://github.com/yourorg/archivmail.git}"
# ── Banner ────────────────────────────────────────────────────────────────────
echo "" echo ""
echo " ╔══════════════════════════════════════╗" echo " ╔══════════════════════════════════════════╗"
echo " ║ archivmail Installer v1.1 ║" echo " ║ archivmail Installer v2.0 ║"
echo " ╚══════════════════════════════════════╝" echo " ╚══════════════════════════════════════════╝"
echo "" echo ""
info "Hostname: $FQDN" info "Hostname: $FQDN"
echo "" echo ""
# ── 1. Pakete ───────────────────────────────────────────────────────────────── # ── Modus-Auswahl ─────────────────────────────────────────────────────────────
INSTALL_MODE="${INSTALL_MODE:-}"
if [[ -z "$INSTALL_MODE" ]]; then
echo " Installations-Modus wählen:"
echo ""
echo " 1) Nativ — Go/Node.js direkt auf dem Server kompiliert und gebaut"
echo " (systemd-Dienste, PostgreSQL lokal, kein Docker nötig)"
echo ""
echo " 2) Docker — Alle Abhängigkeiten (Go, Xapian, Node.js, PostgreSQL)"
echo " laufen in Containern. Kein Build-Toolchain auf dem Host."
echo " Deployment via GitHub Webhook (git pull → docker compose up)"
echo ""
read -rp " Auswahl [1/2]: " _choice
case "$_choice" in
1) INSTALL_MODE="native" ;;
2) INSTALL_MODE="docker" ;;
*) die "Ungültige Auswahl: $_choice" ;;
esac
fi
[[ "$INSTALL_MODE" == "native" || "$INSTALL_MODE" == "docker" ]] \
|| die "INSTALL_MODE muss 'native' oder 'docker' sein (aktuell: $INSTALL_MODE)"
echo ""
info "Modus: $INSTALL_MODE"
echo ""
# ══════════════════════════════════════════════════════════════════════════════
# DOCKER-INSTALLATION
# ══════════════════════════════════════════════════════════════════════════════
install_docker() {
# GitHub-Repo abfragen wenn nicht gesetzt
if [[ "$REPO_URL" == *"yourorg"* ]]; then
read -rp " GitHub-Repository URL (z.B. https://github.com/org/archivmail.git): " REPO_URL
[[ -n "$REPO_URL" ]] || die "Kein Repository angegeben."
fi
# ── Docker installieren ───────────────────────────────────────────────────
info "Installiere Docker..."
if command -v docker &>/dev/null; then
log "Docker bereits installiert: $(docker --version)"
else
apt-get update -qq
apt-get install -y -qq ca-certificates curl gnupg
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg \
| gpg --dearmor -o /etc/apt/keyrings/docker.gpg
chmod a+r /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/debian $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
> /etc/apt/sources.list.d/docker.list
apt-get update -qq
apt-get install -y -qq docker-ce docker-ce-cli containerd.io docker-compose-plugin
systemctl enable --now docker
log "Docker $(docker --version) installiert"
fi
# ── Hilfspakete ───────────────────────────────────────────────────────────
info "Installiere Hilfspakete..."
apt-get install -y -qq git curl nginx openssl logrotate
log "Pakete installiert"
# ── webhook-Binary (adnanh/webhook) ──────────────────────────────────────
info "Installiere webhook-Empfänger..."
if ! command -v webhook &>/dev/null; then
WEBHOOK_BIN_VERSION="2.8.2"
_arch="$(dpkg --print-architecture)"
[[ "$_arch" == "amd64" ]] && _arch="linux_amd64"
[[ "$_arch" == "arm64" ]] && _arch="linux_arm64"
curl -fsSL \
"https://github.com/adnanh/webhook/releases/download/${WEBHOOK_BIN_VERSION}/webhook-${_arch}.tar.gz" \
| tar -xz -C /usr/local/bin --strip-components=1 "webhook-${_arch}/webhook"
chmod +x /usr/local/bin/webhook
log "webhook ${WEBHOOK_BIN_VERSION} installiert"
else
log "webhook bereits installiert"
fi
# ── Verzeichnisse ─────────────────────────────────────────────────────────
info "Erstelle Verzeichnisse..."
mkdir -p "$CONFIG_DIR" "$LOG_DIR"
log "Verzeichnisse erstellt"
# ── Repository klonen ─────────────────────────────────────────────────────
info "Klone Repository..."
if [[ -d "$INSTALL_DIR/.git" ]]; then
log "Repository existiert bereits — git pull"
git -C "$INSTALL_DIR" pull origin main
else
git clone "$REPO_URL" "$INSTALL_DIR"
log "Repository geklont: $INSTALL_DIR"
fi
# ── Keyfile ───────────────────────────────────────────────────────────────
info "Generiere Verschlüsselungs-Keyfile..."
if [[ ! -f "$CONFIG_DIR/keyfile" ]]; then
openssl rand -base64 32 > "$CONFIG_DIR/keyfile"
chmod 400 "$CONFIG_DIR/keyfile"
log "Keyfile generiert: $CONFIG_DIR/keyfile"
else
log "Keyfile existiert bereits wird nicht überschrieben"
fi
# ── TLS-Zertifikat ────────────────────────────────────────────────────────
info "Erstelle selbstsigniertes TLS-Zertifikat..."
mkdir -p "$SSL_DIR"
if [[ ! -f "$SSL_DIR/archivmail.crt" ]]; then
SERVER_IP="$(hostname -I | awk '{print $1}')"
openssl req -x509 -nodes -days 3650 -newkey rsa:4096 \
-keyout "$SSL_DIR/archivmail.key" \
-out "$SSL_DIR/archivmail.crt" \
-subj "/CN=${FQDN}/O=archivmail/C=DE" \
-addext "subjectAltName=DNS:${FQDN},DNS:$(hostname -s),IP:${SERVER_IP}" \
2>/dev/null
chmod 640 "$SSL_DIR/archivmail.key"
chmod 644 "$SSL_DIR/archivmail.crt"
log "TLS-Zertifikat erstellt"
else
log "TLS-Zertifikat existiert bereits wird nicht überschrieben"
fi
# ── config.yml aus Vorlage ────────────────────────────────────────────────
info "Erstelle Konfigurationsdatei..."
if [[ ! -f "$CONFIG_DIR/config.yml" ]]; then
sed \
-e "s/CHANGE_ME_DB_PASSWORD/$DB_PASSWORD/g" \
-e "s/CHANGE_ME_64_CHAR_SECRET/$API_SECRET/g" \
-e "s/mail\.example\.com/$FQDN/g" \
"$INSTALL_DIR/config/config.docker.yml.example" \
> "$CONFIG_DIR/config.yml"
chmod 640 "$CONFIG_DIR/config.yml"
log "config.yml erstellt: $CONFIG_DIR/config.yml"
else
log "config.yml existiert bereits wird nicht überschrieben"
fi
# ── .env für docker-compose ───────────────────────────────────────────────
info "Erstelle .env..."
if [[ ! -f "$INSTALL_DIR/.env" ]]; then
printf 'DB_PASSWORD=%s\n' "$DB_PASSWORD" > "$INSTALL_DIR/.env"
chmod 600 "$INSTALL_DIR/.env"
log ".env erstellt"
else
log ".env existiert bereits wird nicht überschrieben"
fi
# ── deploy.sh ausführbar ──────────────────────────────────────────────────
chmod +x "$INSTALL_DIR/scripts/deploy.sh"
# ── logrotate ─────────────────────────────────────────────────────────────
cat > /etc/logrotate.d/archivmail <<EOF
${LOG_DIR}/deploy.log {
weekly
rotate 12
compress
delaycompress
missingok
notifempty
}
EOF
log "logrotate konfiguriert"
# ── webhook systemd-Dienst ────────────────────────────────────────────────
info "Richte webhook-Dienst ein..."
cat > /etc/systemd/system/archivmail-webhook.service <<EOF
[Unit]
Description=archivmail GitHub Webhook Receiver
After=network.target
[Service]
Type=simple
User=root
WorkingDirectory=$INSTALL_DIR
Environment="WEBHOOK_SECRET=$WEBHOOK_SECRET"
Environment="INSTALL_DIR=$INSTALL_DIR"
ExecStart=/usr/local/bin/webhook \\
-hooks $INSTALL_DIR/scripts/webhook-hooks.json \\
-port 9000 \\
-ip 127.0.0.1 \\
-verbose
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now archivmail-webhook
log "archivmail-webhook Dienst gestartet"
# ── nginx ─────────────────────────────────────────────────────────────────
info "Konfiguriere nginx..."
cat > /etc/nginx/sites-available/archivmail <<EOF
# HTTP → HTTPS Redirect
server {
listen 80;
server_name ${FQDN} _;
return 301 https://\$host\$request_uri;
}
# HTTPS
server {
listen 443 ssl;
http2 on;
server_name ${FQDN} _;
ssl_certificate ${SSL_DIR}/archivmail.crt;
ssl_certificate_key ${SSL_DIR}/archivmail.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options SAMEORIGIN always;
# GitHub Webhook
location /hooks/ {
proxy_pass http://127.0.0.1:9000/hooks/;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_read_timeout 30s;
}
# Go API
location /api/ {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
client_max_body_size 512M;
}
# Next.js Frontend
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
}
access_log /var/log/nginx/archivmail.access.log;
error_log /var/log/nginx/archivmail.error.log;
}
EOF
ln -sf /etc/nginx/sites-available/archivmail /etc/nginx/sites-enabled/archivmail
rm -f /etc/nginx/sites-enabled/default
nginx -t && systemctl enable --now nginx && systemctl reload nginx
log "nginx konfiguriert"
# ── Docker Compose Stack starten ──────────────────────────────────────────
info "Baue und starte Docker Compose Stack..."
cd "$INSTALL_DIR"
docker compose up -d --build
log "Stack gestartet"
# ── Zusammenfassung speichern ─────────────────────────────────────────────
local summary_file="$CONFIG_DIR/install-summary.txt"
cat > "$summary_file" <<EOF
archivmail Docker-Installation — $(date '+%d.%m.%Y %H:%M:%S')
Server: $FQDN
=== ZUGANGSDATEN ===
Datenbank (PostgreSQL im Container):
Passwort: $DB_PASSWORD
API-Secret (JWT + AES):
Secret: $API_SECRET
GitHub Webhook:
Secret: $WEBHOOK_SECRET
Web-Logins werden beim ersten Container-Start angelegt.
Passwörter: docker logs archivmail | grep -E 'admin|auditor'
=== DIENSTE ===
Web: https://$FQDN
SMTP: $FQDN:2525
IMAP: $FQDN:1143 (nginx → TLS)
=== GITHUB WEBHOOK EINRICHTEN ===
URL: https://$FQDN/hooks/deploy
Content-Type: application/json
Secret: $WEBHOOK_SECRET
Events: Just the push event (refs/heads/main)
=== DATEIPFADE ===
Konfiguration: $CONFIG_DIR/config.yml
Keyfile: $CONFIG_DIR/keyfile (NIEMALS löschen!)
TLS-Cert: $SSL_DIR/archivmail.crt
Repository: $INSTALL_DIR
Deploy-Log: $LOG_DIR/deploy.log
=== NÜTZLICHE BEFEHLE ===
docker compose -f $INSTALL_DIR/docker-compose.yml logs -f
docker compose -f $INSTALL_DIR/docker-compose.yml ps
bash $INSTALL_DIR/scripts/deploy.sh # Manueller Deploy
EOF
chmod 600 "$summary_file"
log "Zusammenfassung: $summary_file"
# ── Abschluss ─────────────────────────────────────────────────────────────
echo ""
echo " ╔══════════════════════════════════════════════════════════════╗"
echo " ║ Docker-Installation abgeschlossen! ║"
echo " ╚══════════════════════════════════════════════════════════════╝"
echo ""
log "archivmail läuft unter: https://$FQDN"
echo ""
warn "GitHub Webhook jetzt einrichten:"
echo " URL: https://$FQDN/hooks/deploy"
echo " Content-Type: application/json"
echo " Secret: $WEBHOOK_SECRET"
echo " Events: Push (nur main-Branch)"
echo ""
warn "Initial-Passwörter: docker logs archivmail | grep -E 'admin|auditor'"
warn "Zusammenfassung: $CONFIG_DIR/install-summary.txt"
echo ""
}
# ══════════════════════════════════════════════════════════════════════════════
# NATIVE INSTALLATION (systemd, direkter Build)
# ══════════════════════════════════════════════════════════════════════════════
install_native() {
# ── 1. Pakete ─────────────────────────────────────────────────────────────
info "Installiere Systempakete..." info "Installiere Systempakete..."
apt-get update -qq apt-get update -qq
apt-get install -y -qq \ apt-get install -y -qq \
@@ -52,13 +399,13 @@ if ! command -v go >/dev/null 2>&1; then
fi fi
log "go $(go version | awk '{print $3}')" log "go $(go version | awk '{print $3}')"
# ── 2. Systembenutzer ───────────────────────────────────────────────────────── # ── 2. Systembenutzer ─────────────────────────────────────────────────────
info "Lege Systembenutzer '$AM_USER' an..." info "Lege Systembenutzer '$AM_USER' an..."
id "$AM_USER" &>/dev/null \ id "$AM_USER" &>/dev/null \
&& log "Benutzer '$AM_USER' existiert bereits" \ && log "Benutzer '$AM_USER' existiert bereits" \
|| { useradd --system --shell /bin/false --home "$STORE_DIR" --create-home "$AM_USER"; log "Benutzer angelegt"; } || { useradd --system --shell /bin/false --home "$STORE_DIR" --create-home "$AM_USER"; log "Benutzer angelegt"; }
# ── 3. Verzeichnisstruktur ──────────────────────────────────────────────────── # ── 3. Verzeichnisstruktur ────────────────────────────────────────────────
info "Erstelle Verzeichnisstruktur..." info "Erstelle Verzeichnisstruktur..."
mkdir -p "$STORE_DIR/store" "$STORE_DIR/astore" "$STORE_DIR/xapian" mkdir -p "$STORE_DIR/store" "$STORE_DIR/astore" "$STORE_DIR/xapian"
mkdir -p "$CONFIG_DIR" "$LOG_DIR" "$INSTALL_DIR" "$INSTALL_DIR/web" "$SSL_DIR" mkdir -p "$CONFIG_DIR" "$LOG_DIR" "$INSTALL_DIR" "$INSTALL_DIR/web" "$SSL_DIR"
@@ -67,7 +414,7 @@ chmod 755 "$STORE_DIR"
chmod 700 "$STORE_DIR/store" "$STORE_DIR/astore" "$STORE_DIR/xapian" chmod 700 "$STORE_DIR/store" "$STORE_DIR/astore" "$STORE_DIR/xapian"
log "Verzeichnisse erstellt" log "Verzeichnisse erstellt"
# ── 4. Keyfile ──────────────────────────────────────────────────────────────── # ── 4. Keyfile ────────────────────────────────────────────────────────────
info "Generiere Verschlüsselungs-Keyfile..." info "Generiere Verschlüsselungs-Keyfile..."
if [ ! -f "$CONFIG_DIR/keyfile" ]; then if [ ! -f "$CONFIG_DIR/keyfile" ]; then
openssl rand -base64 32 > "$CONFIG_DIR/keyfile" openssl rand -base64 32 > "$CONFIG_DIR/keyfile"
@@ -78,7 +425,7 @@ else
log "Keyfile existiert bereits wird nicht überschrieben" log "Keyfile existiert bereits wird nicht überschrieben"
fi fi
# ── 5. TLS-Zertifikat ───────────────────────────────────────────────────────── # ── 5. TLS-Zertifikat ─────────────────────────────────────────────────────
info "Erstelle selbstsigniertes TLS-Zertifikat..." info "Erstelle selbstsigniertes TLS-Zertifikat..."
if [ ! -f "$SSL_DIR/archivmail.crt" ]; then if [ ! -f "$SSL_DIR/archivmail.crt" ]; then
SERVER_IP="$(hostname -I | awk '{print $1}')" SERVER_IP="$(hostname -I | awk '{print $1}')"
@@ -96,7 +443,7 @@ else
log "TLS-Zertifikat existiert bereits wird nicht überschrieben" log "TLS-Zertifikat existiert bereits wird nicht überschrieben"
fi fi
# ── 6. PostgreSQL ───────────────────────────────────────────────────────────── # ── 6. PostgreSQL ─────────────────────────────────────────────────────────
info "Richte PostgreSQL ein..." info "Richte PostgreSQL ein..."
systemctl enable postgresql --quiet systemctl enable postgresql --quiet
systemctl start postgresql systemctl start postgresql
@@ -112,16 +459,13 @@ su -c "psql -tc \"SELECT 1 FROM pg_roles WHERE rolname='archivmail'\" | grep -q
|| psql -c \"CREATE USER archivmail WITH PASSWORD '$DB_PASSWORD'\"" postgres || psql -c \"CREATE USER archivmail WITH PASSWORD '$DB_PASSWORD'\"" postgres
su -c "psql -tc \"SELECT 1 FROM pg_database WHERE datname='archivmail'\" | grep -q 1 || \ su -c "psql -tc \"SELECT 1 FROM pg_database WHERE datname='archivmail'\" | grep -q 1 || \
psql -c \"CREATE DATABASE archivmail OWNER archivmail\"" postgres psql -c \"CREATE DATABASE archivmail OWNER archivmail\"" postgres
su -c "psql archivmail -c \"GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO archivmail;\"" postgres su -c "psql archivmail -c \"GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO archivmail;\"" postgres
su -c "psql archivmail -c \"GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO archivmail;\"" postgres su -c "psql archivmail -c \"GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO archivmail;\"" postgres
su -c "psql archivmail -c \"ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO archivmail;\"" postgres su -c "psql archivmail -c \"ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO archivmail;\"" postgres
su -c "psql archivmail -c \"ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO archivmail;\"" postgres su -c "psql archivmail -c \"ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO archivmail;\"" postgres
log "PostgreSQL eingerichtet" log "PostgreSQL eingerichtet"
log "Standard-Benutzer werden beim ersten Daemon-Start angelegt"
# ── 7. Konfiguration ────────────────────────────────────────────────────────── # ── 7. Konfiguration ──────────────────────────────────────────────────────
info "Erstelle Konfigurationsdatei..." info "Erstelle Konfigurationsdatei..."
if [ ! -f "$CONFIG_DIR/config.yml" ]; then if [ ! -f "$CONFIG_DIR/config.yml" ]; then
cat > "$CONFIG_DIR/config.yml" << CONFIG cat > "$CONFIG_DIR/config.yml" << CONFIG
@@ -182,8 +526,8 @@ else
log "config.yml existiert bereits wird nicht überschrieben" log "config.yml existiert bereits wird nicht überschrieben"
fi fi
# ── 8. Nginx (HTTP + HTTPS) ──────────────────────────────────────────────────── # ── 8. Nginx ──────────────────────────────────────────────────────────────
info "Konfiguriere Nginx (HTTP → HTTPS Redirect + TLS)..." info "Konfiguriere nginx (HTTP → HTTPS + TLS)..."
cat > /etc/nginx/sites-available/archivmail << NGINX cat > /etc/nginx/sites-available/archivmail << NGINX
# HTTP → HTTPS Redirect # HTTP → HTTPS Redirect
server { server {
@@ -241,9 +585,9 @@ rm -f /etc/nginx/sites-enabled/default
nginx -t nginx -t
systemctl enable nginx --quiet systemctl enable nginx --quiet
systemctl restart nginx systemctl restart nginx
log "Nginx konfiguriert (HTTP→HTTPS, TLS)" log "nginx konfiguriert"
# ── 9. logrotate ────────────────────────────────────────────────────────────── # ── 9. logrotate ──────────────────────────────────────────────────────────
cat > /etc/logrotate.d/archivmail << LOGROTATE cat > /etc/logrotate.d/archivmail << LOGROTATE
${LOG_DIR}/audit.log { ${LOG_DIR}/audit.log {
daily daily
@@ -257,7 +601,7 @@ ${LOG_DIR}/audit.log {
LOGROTATE LOGROTATE
log "logrotate konfiguriert" log "logrotate konfiguriert"
# ── 10. systemd Units ───────────────────────────────────────────────────────── # ── 10. systemd Units ─────────────────────────────────────────────────────
info "Erstelle systemd Units..." info "Erstelle systemd Units..."
cat > /etc/systemd/system/archivmail.service << UNIT cat > /etc/systemd/system/archivmail.service << UNIT
[Unit] [Unit]
@@ -314,18 +658,24 @@ systemctl daemon-reload
systemctl enable archivmail archivmail-web --quiet systemctl enable archivmail archivmail-web --quiet
log "systemd Units erstellt und aktiviert" log "systemd Units erstellt und aktiviert"
# ── 11. update.sh installieren und erstes Deployment ────────────────────────── # ── 11. Erstes Deployment via update.sh ───────────────────────────────────
info "Installiere update.sh und führe erstes Deployment durch..." info "Installiere update.sh und führe erstes Deployment durch..."
curl -fsSL "${REPO_URL/\.git/}/raw/branch/main/update.sh" -o "$INSTALL_DIR/update.sh" 2>/dev/null \ _update_url="${REPO_URL/\.git/}/raw/branch/main/update.sh"
|| { warn "update.sh konnte nicht von Gitea geladen werden überspringe Deployment"; } # GitHub-URLs anpassen (raw.githubusercontent.com statt github.com/raw/...)
if [[ "$REPO_URL" == *"github.com"* ]]; then
_repo_path="${REPO_URL#*github.com/}"
_repo_path="${_repo_path%.git}"
_update_url="https://raw.githubusercontent.com/${_repo_path}/main/update.sh"
fi
curl -fsSL "$_update_url" -o "$INSTALL_DIR/update.sh" 2>/dev/null \
|| { warn "update.sh konnte nicht geladen werden überspringe Deployment"; }
if [ -f "$INSTALL_DIR/update.sh" ]; then if [ -f "$INSTALL_DIR/update.sh" ]; then
chmod +x "$INSTALL_DIR/update.sh" chmod +x "$INSTALL_DIR/update.sh"
log "update.sh installiert: $INSTALL_DIR/update.sh" log "update.sh installiert"
info "Führe erstes Deployment durch (Build + Start)..." info "Führe erstes Deployment durch..."
bash "$INSTALL_DIR/update.sh" bash "$INSTALL_DIR/update.sh"
# Echte Passwörter aus Journal lesen (Backend gibt sie beim ersten Start aus)
# Auf Backend warten (bis zu 15 Sekunden)
for _i in $(seq 1 15); do for _i in $(seq 1 15); do
_pw=$(journalctl -u archivmail --no-pager -n 100 | grep 'admin' | grep -oP ':\s+\K[0-9a-f]{16,}' | tail -1) _pw=$(journalctl -u archivmail --no-pager -n 100 | grep 'admin' | grep -oP ':\s+\K[0-9a-f]{16,}' | tail -1)
[[ -n "$_pw" ]] && break [[ -n "$_pw" ]] && break
@@ -334,42 +684,29 @@ if [ -f "$INSTALL_DIR/update.sh" ]; then
PW_SUPERADMIN=$(journalctl -u archivmail --no-pager -n 100 | grep 'superadmin' | grep -oP ':\s+\K\S+' | tail -1) PW_SUPERADMIN=$(journalctl -u archivmail --no-pager -n 100 | grep 'superadmin' | grep -oP ':\s+\K\S+' | tail -1)
PW_ADMIN=$(journalctl -u archivmail --no-pager -n 100 | grep ' admin ' | grep -v superadmin | grep -oP ':\s+\K\S+' | tail -1) PW_ADMIN=$(journalctl -u archivmail --no-pager -n 100 | grep ' admin ' | grep -v superadmin | grep -oP ':\s+\K\S+' | tail -1)
PW_AUDITOR=$(journalctl -u archivmail --no-pager -n 100 | grep 'auditor' | grep -oP ':\s+\K\S+' | tail -1) PW_AUDITOR=$(journalctl -u archivmail --no-pager -n 100 | grep 'auditor' | grep -oP ':\s+\K\S+' | tail -1)
[[ -n "$PW_ADMIN" ]] || PW_ADMIN="(nicht gefunden — siehe: journalctl -u archivmail | grep admin)" [[ -n "$PW_ADMIN" ]] || PW_ADMIN="(nicht gefunden — journalctl -u archivmail | grep admin)"
[[ -n "$PW_AUDITOR" ]] || PW_AUDITOR="(nicht gefunden — siehe: journalctl -u archivmail | grep auditor)" [[ -n "$PW_AUDITOR" ]] || PW_AUDITOR="(nicht gefunden — journalctl -u archivmail | grep auditor)"
[[ -n "$PW_SUPERADMIN" ]] || PW_SUPERADMIN="(nicht gefunden — siehe: journalctl -u archivmail | grep superadmin)" [[ -n "$PW_SUPERADMIN" ]] || PW_SUPERADMIN="(nicht gefunden — journalctl -u archivmail | grep superadmin)"
else else
warn "update.sh fehlt — manuell ausführen: bash $INSTALL_DIR/update.sh" warn "update.sh fehlt — manuell: bash $INSTALL_DIR/update.sh"
PW_SUPERADMIN="(noch nicht generiert)" PW_SUPERADMIN="(noch nicht generiert)"; PW_ADMIN="(noch nicht generiert)"; PW_AUDITOR="(noch nicht generiert)"
PW_ADMIN="(noch nicht generiert)"
PW_AUDITOR="(noch nicht generiert)"
fi fi
# ── Zusammenfassung in Datei speichern ──────────────────────────────────────── # ── Zusammenfassung ───────────────────────────────────────────────────────
SUMMARY_FILE="$CONFIG_DIR/install-summary.txt" SUMMARY_FILE="$CONFIG_DIR/install-summary.txt"
cat > "$SUMMARY_FILE" << SUMMARY cat > "$SUMMARY_FILE" << SUMMARY
archivmail Installation — $(date '+%d.%m.%Y %H:%M:%S') archivmail Native-Installation — $(date '+%d.%m.%Y %H:%M:%S')
Server: $FQDN Server: $FQDN
=== ANGELEGTE BENUTZER & PASSWÖRTER === === ZUGANGSDATEN ===
Systembenutzer (Linux): Datenbank: archivmail / $DB_PASSWORD
Benutzer: $AM_USER API-Secret: $API_SECRET
Shell: /bin/false (kein Login)
Home: $STORE_DIR
Datenbank (PostgreSQL): Web-Logins (UNBEDINGT ÄNDERN!):
Datenbank: archivmail superadmin@archivmail / $PW_SUPERADMIN
Benutzer: archivmail admin@archivmail / $PW_ADMIN
Passwort: $DB_PASSWORD auditor@archivmail / $PW_AUDITOR
Verbindung: 127.0.0.1:5432
API-Secret (JWT + AES-Schlüsselableitung):
Secret: $API_SECRET
Web-Anwendung (UNBEDINGT ÄNDERN!):
Superadmin: superadmin@archivmail / $PW_SUPERADMIN
Admin: admin@archivmail / $PW_ADMIN
Auditor: auditor@archivmail / $PW_AUDITOR
=== DIENSTE === === DIENSTE ===
@@ -382,38 +719,22 @@ Server: $FQDN
Konfiguration: $CONFIG_DIR/config.yml Konfiguration: $CONFIG_DIR/config.yml
Keyfile: $CONFIG_DIR/keyfile Keyfile: $CONFIG_DIR/keyfile
TLS-Zertifikat: $SSL_DIR/archivmail.crt TLS-Zertifikat: $SSL_DIR/archivmail.crt
TLS-Schlüssel: $SSL_DIR/archivmail.key
Mail-Speicher: $STORE_DIR/ Mail-Speicher: $STORE_DIR/
Logs: $LOG_DIR/ Logs: $LOG_DIR/
Updater: $INSTALL_DIR/update.sh Updater: $INSTALL_DIR/update.sh
=== HINWEISE ===
- Das TLS-Zertifikat ist selbstsigniert (10 Jahre gültig).
Im Browser: Ausnahme hinzufügen oder Zertifikat importieren.
Für vertrauenswürdiges Zertifikat: Admin → Zertifikat → Let's Encrypt
- DB-Passwort und API-Secret stehen in $CONFIG_DIR/config.yml (chmod 640).
- Keyfile NIEMALS löschen oder überschreiben — alle Mails werden damit entschlüsselt.
- Diese Datei nach dem ersten Login löschen oder sicher aufbewahren!
SUMMARY SUMMARY
chmod 600 "$SUMMARY_FILE" chmod 600 "$SUMMARY_FILE"
log "Zusammenfassung gespeichert: $SUMMARY_FILE" log "Zusammenfassung: $SUMMARY_FILE"
# ── Abschlussbericht ────────────────────────────────────────────────────────── # ── Abschluss ─────────────────────────────────────────────────────────────
echo "" echo ""
echo " ╔══════════════════════════════════════════════════════════╗" echo " ╔══════════════════════════════════════════════════════════╗"
echo " ║ Installation abgeschlossen! ║" echo " ║ Installation abgeschlossen! ║"
echo " ╚══════════════════════════════════════════════════════════╝" echo " ╚══════════════════════════════════════════════════════════╝"
echo "" echo ""
echo " Hostname (FQDN): $FQDN" echo " Web (HTTPS): https://$FQDN"
echo "" echo " IMAP (TLS): $FQDN:993"
echo " Pfade:" echo " SMTP: $FQDN:2525"
echo " Binaries: $INSTALL_DIR/"
echo " Mail-Speicher: $STORE_DIR/"
echo " Konfiguration: $CONFIG_DIR/config.yml"
echo " Keyfile: $CONFIG_DIR/keyfile (chmod 400 sicher aufbewahren!)"
echo " TLS-Zertifikat: $SSL_DIR/archivmail.crt"
echo " Logs: $LOG_DIR/"
echo "" echo ""
echo " ┌─────────────────────────────────────────────────────────┐" echo " ┌─────────────────────────────────────────────────────────┐"
echo " │ ANGELEGTE BENUTZER & PASSWÖRTER │" echo " │ ANGELEGTE BENUTZER & PASSWÖRTER │"
@@ -424,20 +745,17 @@ echo " │ Web-Logins (UNBEDINGT ÄNDERN!): │"
printf " │ superadmin / %-38s│\n" "$PW_SUPERADMIN" printf " │ superadmin / %-38s│\n" "$PW_SUPERADMIN"
printf " │ admin / %-38s│\n" "$PW_ADMIN" printf " │ admin / %-38s│\n" "$PW_ADMIN"
printf " │ auditor / %-38s│\n" "$PW_AUDITOR" printf " │ auditor / %-38s│\n" "$PW_AUDITOR"
echo " ├─────────────────────────────────────────────────────────┤"
printf " │ API-Secret: %-38s │\n" "${API_SECRET:0:38}"
echo " └─────────────────────────────────────────────────────────┘" echo " └─────────────────────────────────────────────────────────┘"
echo "" echo ""
echo " Vollständige Zusammenfassung: $SUMMARY_FILE" warn "Zusammenfassung mit Passwörtern: $SUMMARY_FILE"
echo ""
echo " Dienste:"
echo " Web (HTTPS): https://$FQDN"
echo " IMAP (TLS): $FQDN:993"
echo " SMTP: $FQDN:2525"
echo ""
echo " Hinweis: Das TLS-Zertifikat ist selbstsigniert."
echo " Für ein vertrauenswürdiges Zertifikat: Admin → Zertifikat → Let's Encrypt"
echo ""
warn "Zusammenfassung mit Passwörtern: $SUMMARY_FILE (chmod 600)"
warn "Standardpasswörter unbedingt nach dem ersten Login ändern!" warn "Standardpasswörter unbedingt nach dem ersten Login ändern!"
echo "" echo ""
}
# ══════════════════════════════════════════════════════════════════════════════
# MODUS AUSFÜHREN
# ══════════════════════════════════════════════════════════════════════════════
case "$INSTALL_MODE" in
docker) install_docker ;;
native) install_native ;;
esac
+31
View File
@@ -0,0 +1,31 @@
#!/bin/bash
# deploy.sh — Wird vom Webhook-Dienst bei GitHub-Push auf main aufgerufen.
# Läuft auf dem HOST (nicht im Container).
set -euo pipefail
INSTALL_DIR="${INSTALL_DIR:-/opt/archivmail}"
LOG_FILE="/var/log/archivmail/deploy.log"
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"; }
log "=== Deploy gestartet (von GitHub Webhook) ==="
cd "$INSTALL_DIR"
# 1. Aktuellen Stand holen
log "git pull origin main..."
git pull origin main
# 2. Neue Images bauen
log "docker compose build..."
docker compose build --no-cache
# 3. Stack neustarten (zero-downtime: postgres bleibt laufen)
log "docker compose up -d..."
docker compose up -d --remove-orphans
# 4. Alte ungenutzte Images aufräumen
docker image prune -f
log "=== Deploy abgeschlossen ==="
+32
View File
@@ -0,0 +1,32 @@
[
{
"id": "deploy",
"execute-command": "/opt/archivmail/scripts/deploy.sh",
"command-working-directory": "/opt/archivmail",
"response-message": "Deploy gestartet.",
"trigger-rule": {
"and": [
{
"match": {
"type": "payload-hmac-sha256",
"secret": "{{ getenv \"WEBHOOK_SECRET\" }}",
"parameter": {
"source": "header",
"name": "X-Hub-Signature-256"
}
}
},
{
"match": {
"type": "value",
"value": "refs/heads/main",
"parameter": {
"source": "payload",
"name": "ref"
}
}
}
]
}
}
]