feat(Docker): Upgrade to PHP 8.5.0RC3 with native ext-uri support
BREAKING CHANGE: Requires PHP 8.5.0RC3 Changes: - Update Docker base image from php:8.4-fpm to php:8.5.0RC3-fpm - Enable ext-uri for native WHATWG URL parsing support - Update composer.json PHP requirement from ^8.4 to ^8.5 - Add ext-uri as required extension in composer.json - Move URL classes from Url.php85/ to Url/ directory (now compatible) - Remove temporary PHP 8.4 compatibility workarounds Benefits: - Native URL parsing with Uri\WhatWg\Url class - Better performance for URL operations - Future-proof with latest PHP features - Eliminates PHP version compatibility issues
This commit is contained in:
239
deployment/infrastructure/playbooks/README-git-deployment.md
Normal file
239
deployment/infrastructure/playbooks/README-git-deployment.md
Normal file
@@ -0,0 +1,239 @@
|
||||
# Git-Based Deployment mit Gitea
|
||||
|
||||
## Übersicht
|
||||
|
||||
Das Git-basierte Deployment Playbook (`deploy-git-based.yml`) ermöglicht Zero-Downtime Deployments mit Gitea als Git-Repository-Server.
|
||||
|
||||
## Voraussetzungen
|
||||
|
||||
### 1. Gitea Server Setup
|
||||
|
||||
Der Gitea Server muss für den Production-Server erreichbar sein. Es gibt zwei Optionen:
|
||||
|
||||
#### Option A: Öffentlich erreichbarer Gitea Server (Empfohlen für Production)
|
||||
|
||||
```bash
|
||||
# Gitea muss über das Internet erreichbar sein
|
||||
git_repo: "git@git.michaelschiemer.de:michael/michaelschiemer.git"
|
||||
```
|
||||
|
||||
**Erforderlich**:
|
||||
- Öffentliche IP oder Domain für Gitea
|
||||
- Firewall-Regel für Port 2222 (SSH)
|
||||
- SSL/TLS für Webinterface (Port 9443/3000)
|
||||
|
||||
#### Option B: Gitea auf dem Production-Server
|
||||
|
||||
```bash
|
||||
# Gitea läuft auf demselben Server wie die Anwendung
|
||||
git_repo: "git@localhost:michael/michaelschiemer.git"
|
||||
```
|
||||
|
||||
**Erforderlich**:
|
||||
- Gitea Container auf Production-Server deployen
|
||||
- Docker Compose Setup auf Production-Server
|
||||
- Lokale SSH-Konfiguration
|
||||
|
||||
### 2. SSH Key Setup
|
||||
|
||||
Der Deploy-User auf dem Production-Server benötigt einen SSH-Key:
|
||||
|
||||
```bash
|
||||
# Auf dem Production-Server
|
||||
ssh-keygen -t ed25519 -C "deployment@michaelschiemer" -f ~/.ssh/gitea_deploy_key -N ""
|
||||
|
||||
# Public Key zu Gitea hinzufügen (via Web-UI oder API)
|
||||
cat ~/.ssh/gitea_deploy_key.pub
|
||||
```
|
||||
|
||||
### 3. SSH Keys im Secrets-Verzeichnis
|
||||
|
||||
Die SSH Keys müssen im `deployment/infrastructure/secrets/` Verzeichnis liegen:
|
||||
|
||||
```bash
|
||||
deployment/infrastructure/secrets/
|
||||
├── .gitignore # Schützt Keys vor versehentlichem Commit
|
||||
├── gitea_deploy_key # Private Key
|
||||
└── gitea_deploy_key.pub # Public Key
|
||||
```
|
||||
|
||||
**WICHTIG**: Das `secrets/` Verzeichnis ist via `.gitignore` geschützt und darf NIEMALS committed werden!
|
||||
|
||||
## Deployment-Ablauf
|
||||
|
||||
### 1. SSH Key auf Production-Server kopieren
|
||||
|
||||
Das Playbook kopiert automatisch die SSH Keys aus `secrets/` auf den Production-Server:
|
||||
|
||||
```yaml
|
||||
- name: Copy Gitea deploy SSH private key
|
||||
copy:
|
||||
src: "{{ playbook_dir }}/../secrets/gitea_deploy_key"
|
||||
dest: "/home/{{ app_user }}/.ssh/gitea_deploy_key"
|
||||
mode: '0600'
|
||||
```
|
||||
|
||||
### 2. SSH-Konfiguration
|
||||
|
||||
Das Playbook erstellt automatisch die SSH-Konfiguration:
|
||||
|
||||
```ssh
|
||||
Host localhost
|
||||
HostName localhost
|
||||
Port 2222
|
||||
User git
|
||||
IdentityFile ~/.ssh/gitea_deploy_key
|
||||
StrictHostKeyChecking no
|
||||
UserKnownHostsFile /dev/null
|
||||
|
||||
Host git.michaelschiemer.de
|
||||
HostName git.michaelschiemer.de
|
||||
Port 2222
|
||||
User git
|
||||
IdentityFile ~/.ssh/gitea_deploy_key
|
||||
StrictHostKeyChecking no
|
||||
UserKnownHostsFile /dev/null
|
||||
```
|
||||
|
||||
### 3. Git Clone
|
||||
|
||||
Das Playbook clont das Repository in ein Release-Verzeichnis:
|
||||
|
||||
```bash
|
||||
/var/www/michaelschiemer/
|
||||
├── releases/
|
||||
│ ├── 1761524417/ # Timestamp-basierte Releases
|
||||
│ └── v1.0.0/ # Tag-basierte Releases
|
||||
├── shared/ # Shared Directories (symlinked)
|
||||
│ ├── storage/
|
||||
│ └── .env.production
|
||||
└── current -> releases/1761524417 # Symlink auf aktives Release
|
||||
```
|
||||
|
||||
### 4. Zero-Downtime Deployment
|
||||
|
||||
- Neues Release wird geclont
|
||||
- Dependencies installiert
|
||||
- Symlinks erstellt
|
||||
- `current` Symlink atomar gewechselt
|
||||
- Health Check durchgeführt
|
||||
- Bei Fehler: Automatischer Rollback
|
||||
|
||||
## Deployment ausführen
|
||||
|
||||
### Standard Deployment (main Branch)
|
||||
|
||||
```bash
|
||||
cd deployment/infrastructure
|
||||
ansible-playbook -i inventories/production/hosts.yml playbooks/deploy-git-based.yml
|
||||
```
|
||||
|
||||
### Tag-basiertes Deployment
|
||||
|
||||
```bash
|
||||
ansible-playbook -i inventories/production/hosts.yml playbooks/deploy-git-based.yml \
|
||||
--extra-vars "release_tag=v1.0.0"
|
||||
```
|
||||
|
||||
### Custom Branch Deployment
|
||||
|
||||
```bash
|
||||
ansible-playbook -i inventories/production/hosts.yml playbooks/deploy-git-based.yml \
|
||||
--extra-vars "git_branch=develop"
|
||||
```
|
||||
|
||||
## Konfiguration anpassen
|
||||
|
||||
### Git Repository URL ändern
|
||||
|
||||
In `deploy-git-based.yml`:
|
||||
|
||||
```yaml
|
||||
vars:
|
||||
git_repo: "git@git.michaelschiemer.de:michael/michaelschiemer.git"
|
||||
# Oder für lokales Testing:
|
||||
# git_repo: "git@localhost:michael/michaelschiemer.git"
|
||||
```
|
||||
|
||||
### Shared Directories anpassen
|
||||
|
||||
```yaml
|
||||
vars:
|
||||
shared_dirs:
|
||||
- storage/logs
|
||||
- storage/cache
|
||||
- storage/sessions
|
||||
- storage/uploads
|
||||
- public/uploads
|
||||
|
||||
shared_files:
|
||||
- .env.production
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Fehler: "Connection refused" zu Gitea
|
||||
|
||||
**Problem**: Der Production-Server kann Gitea nicht erreichen.
|
||||
|
||||
**Lösung**:
|
||||
1. Prüfe, ob Gitea öffentlich erreichbar ist: `nc -zv git.michaelschiemer.de 2222`
|
||||
2. Prüfe Firewall-Regeln auf dem Gitea-Server
|
||||
3. Für lokales Testing: Verwende rsync-based Deployment stattdessen
|
||||
|
||||
### Fehler: "Permission denied (publickey)"
|
||||
|
||||
**Problem**: SSH Key ist nicht korrekt konfiguriert.
|
||||
|
||||
**Lösung**:
|
||||
1. Prüfe, ob der Public Key in Gitea hinzugefügt wurde
|
||||
2. Prüfe SSH Key Permissions: `chmod 600 ~/.ssh/gitea_deploy_key`
|
||||
3. Teste SSH-Verbindung manuell: `ssh -p 2222 -i ~/.ssh/gitea_deploy_key git@git.michaelschiemer.de`
|
||||
|
||||
### Health Check schlägt fehl
|
||||
|
||||
**Problem**: Deployment-Health-Check failed.
|
||||
|
||||
**Lösung**:
|
||||
1. Automatischer Rollback wurde durchgeführt
|
||||
2. Prüfe Logs: `tail -f /var/www/michaelschiemer/deploy.log`
|
||||
3. Prüfe Application Logs: `/var/www/michaelschiemer/shared/storage/logs/`
|
||||
|
||||
## Comparison: Git-based vs rsync-based
|
||||
|
||||
### Git-based Deployment (Dieser Playbook)
|
||||
|
||||
**Vorteile**:
|
||||
- Zero-Downtime durch Symlink-Switch
|
||||
- Atomare Releases mit Rollback-Fähigkeit
|
||||
- Git-Historie auf Production-Server
|
||||
- Einfache Rollbacks zu vorherigen Releases
|
||||
|
||||
**Nachteile**:
|
||||
- Gitea Server muss erreichbar sein
|
||||
- Zusätzliche Infrastruktur (Gitea)
|
||||
- SSH Key Management erforderlich
|
||||
|
||||
### rsync-based Deployment
|
||||
|
||||
**Vorteile**:
|
||||
- Keine zusätzliche Infrastruktur
|
||||
- Funktioniert mit lokalem Development-Environment
|
||||
- Schneller für kleine Änderungen
|
||||
|
||||
**Nachteile**:
|
||||
- Kein Zero-Downtime ohne zusätzliche Logik
|
||||
- Keine Git-Historie auf Server
|
||||
- Rollback komplizierter
|
||||
|
||||
## Empfehlung
|
||||
|
||||
**Für Production**: Git-based Deployment mit öffentlich erreichbarem Gitea Server
|
||||
**Für Development/Testing**: rsync-based Deployment (bereits implementiert und getestet)
|
||||
|
||||
## Related Files
|
||||
|
||||
- `deploy-git-based.yml` - Git-based Deployment Playbook
|
||||
- `deploy-rsync-based.yml` - rsync-based Deployment Playbook (Alternative)
|
||||
- `rollback-git-based.yml` - Rollback Playbook für Git-Deployments
|
||||
- `secrets/.gitignore` - Schutz für SSH Keys
|
||||
652
deployment/infrastructure/playbooks/README-rsync-deployment.md
Normal file
652
deployment/infrastructure/playbooks/README-rsync-deployment.md
Normal file
@@ -0,0 +1,652 @@
|
||||
# Rsync-Based Deployment
|
||||
|
||||
**Production-ready Zero-Downtime Deployment** mit Rsync, Release Management und automatischem Rollback.
|
||||
|
||||
## Übersicht
|
||||
|
||||
Das Rsync-basierte Deployment Playbook (`deploy-rsync-based.yml`) bietet eine robuste Lösung für Production Deployments ohne externe Git-Server-Abhängigkeiten.
|
||||
|
||||
**Vorteile**:
|
||||
- ✅ Zero-Downtime durch Symlink-Switch
|
||||
- ✅ Automatischer Rollback bei Health Check Failure
|
||||
- ✅ Git Tag-basiertes Release Management
|
||||
- ✅ Keine Gitea/GitHub Abhängigkeit
|
||||
- ✅ Schnell für kleine Änderungen
|
||||
- ✅ Einfaches Rollback zu vorherigen Releases
|
||||
|
||||
## Deployment-Architektur
|
||||
|
||||
### Release Structure
|
||||
|
||||
```
|
||||
/home/deploy/michaelschiemer/
|
||||
├── releases/
|
||||
│ ├── 1761499893/ # Timestamp-based releases
|
||||
│ ├── v1.0.0/ # Git tag-based releases
|
||||
│ └── v1.2.3/
|
||||
├── shared/ # Shared zwischen Releases
|
||||
│ ├── storage/
|
||||
│ │ └── sessions/
|
||||
│ ├── public/
|
||||
│ │ └── uploads/
|
||||
│ └── .env.production # Shared config
|
||||
├── current -> releases/v1.2.3 # Symlink auf aktives Release
|
||||
└── deploy.log # Deployment history
|
||||
```
|
||||
|
||||
### Zero-Downtime Process
|
||||
|
||||
```
|
||||
1. Build Assets (local)
|
||||
↓
|
||||
2. Rsync to new release directory
|
||||
↓
|
||||
3. Create symlinks zu shared directories
|
||||
↓
|
||||
4. Start Docker containers
|
||||
↓
|
||||
5. Health Check (3 retries)
|
||||
├─ Success → Switch 'current' symlink (atomic)
|
||||
└─ Failure → Rollback zu previous release
|
||||
↓
|
||||
6. Cleanup old releases (keep last 5)
|
||||
```
|
||||
|
||||
## Voraussetzungen
|
||||
|
||||
### 1. SSH Key Setup
|
||||
|
||||
SSH Keys für Production Server müssen konfiguriert sein:
|
||||
|
||||
```bash
|
||||
# SSH config in ~/.ssh/config
|
||||
Host michaelschiemer-prod
|
||||
HostName 94.16.110.151
|
||||
User deploy
|
||||
IdentityFile ~/.ssh/production
|
||||
StrictHostKeyChecking no
|
||||
```
|
||||
|
||||
### 2. Production Server Requirements
|
||||
|
||||
- **User**: `deploy` user mit sudo Rechten
|
||||
- **Docker**: Docker und Docker Compose installiert
|
||||
- **Directory**: `/home/deploy/michaelschiemer` mit korrekten Permissions
|
||||
|
||||
### 3. Local Development Setup
|
||||
|
||||
- **Composer**: Für `composer install`
|
||||
- **NPM**: Für `npm run build`
|
||||
- **Git**: Für Tag-basiertes Release Management (optional)
|
||||
- **Ansible**: Ansible ≥2.13 installiert
|
||||
|
||||
## Deployment-Workflows
|
||||
|
||||
### Standard Deployment (Timestamp-based)
|
||||
|
||||
Deployiert aktuellen Stand ohne Git Tag:
|
||||
|
||||
```bash
|
||||
cd deployment/infrastructure
|
||||
ansible-playbook -i inventories/production/hosts.yml playbooks/deploy-rsync-based.yml
|
||||
```
|
||||
|
||||
**Release Name**: Unix Timestamp (z.B. `1761499893`)
|
||||
|
||||
### Tagged Release Deployment (Recommended)
|
||||
|
||||
Deployiert spezifischen Git Tag:
|
||||
|
||||
```bash
|
||||
# Option 1: Tag explizit angeben
|
||||
ansible-playbook -i inventories/production/hosts.yml playbooks/deploy-rsync-based.yml \
|
||||
--extra-vars "release_tag=v1.2.3"
|
||||
|
||||
# Option 2: Aktuellen Git Tag verwenden (auto-detected)
|
||||
git tag v1.2.3
|
||||
git push origin v1.2.3
|
||||
ansible-playbook -i inventories/production/hosts.yml playbooks/deploy-rsync-based.yml
|
||||
```
|
||||
|
||||
**Release Name**: Git Tag (z.B. `v1.2.3`)
|
||||
|
||||
### Force Deployment (Override Lock)
|
||||
|
||||
Wenn ein Deployment Lock existiert:
|
||||
|
||||
```bash
|
||||
ansible-playbook -i inventories/production/hosts.yml playbooks/deploy-rsync-based.yml \
|
||||
--extra-vars "force_deploy=true"
|
||||
```
|
||||
|
||||
## Release Management
|
||||
|
||||
### Git Tag Workflow
|
||||
|
||||
**Semantic Versioning** wird empfohlen:
|
||||
|
||||
```bash
|
||||
# 1. Create Git tag
|
||||
git tag -a v1.2.3 -m "Release v1.2.3: Feature XYZ"
|
||||
git push origin v1.2.3
|
||||
|
||||
# 2. Deploy tagged release
|
||||
ansible-playbook -i inventories/production/hosts.yml playbooks/deploy-rsync-based.yml \
|
||||
--extra-vars "release_tag=v1.2.3"
|
||||
|
||||
# 3. Verify deployment
|
||||
ssh deploy@94.16.110.151 'ls -la /home/deploy/michaelschiemer/releases/'
|
||||
```
|
||||
|
||||
### Auto-Detection von Git Tags
|
||||
|
||||
Wenn `release_tag` nicht angegeben wird, versucht das Playbook automatisch den aktuellen Git Tag zu verwenden:
|
||||
|
||||
```bash
|
||||
# Auf einem getaggten Commit
|
||||
git describe --tags --exact-match # Zeigt: v1.2.3
|
||||
|
||||
# Deployment verwendet automatisch v1.2.3 als Release Name
|
||||
ansible-playbook -i inventories/production/hosts.yml playbooks/deploy-rsync-based.yml
|
||||
```
|
||||
|
||||
**Fallback**: Wenn kein Git Tag vorhanden → Timestamp als Release Name
|
||||
|
||||
### Release List anzeigen
|
||||
|
||||
```bash
|
||||
ansible -i inventories/production/hosts.yml web_servers -b -a \
|
||||
"ls -lt /home/deploy/michaelschiemer/releases | head -10"
|
||||
```
|
||||
|
||||
**Output**:
|
||||
```
|
||||
total 20
|
||||
drwxr-xr-x 10 deploy deploy 4096 Oct 26 18:50 v1.2.3
|
||||
drwxr-xr-x 10 deploy deploy 4096 Oct 25 14:32 v1.2.2
|
||||
drwxr-xr-x 10 deploy deploy 4096 Oct 24 10:15 1761499893
|
||||
drwxr-xr-x 10 deploy deploy 4096 Oct 23 09:00 v1.2.1
|
||||
lrwxrwxrwx 1 deploy deploy 56 Oct 26 18:50 current -> /home/deploy/michaelschiemer/releases/v1.2.3
|
||||
```
|
||||
|
||||
## Rollback Mechanisms
|
||||
|
||||
### Automatic Rollback
|
||||
|
||||
Bei Health Check Failure rollback das Playbook automatisch:
|
||||
|
||||
1. **Stop failed release containers**
|
||||
2. **Switch `current` symlink** zurück zu `previous_release`
|
||||
3. **Start previous release containers**
|
||||
4. **Remove failed release directory**
|
||||
5. **Log rollback event**
|
||||
|
||||
**Trigger**: Health Check Status ≠ 200 (nach 3 Retries mit 5s delay)
|
||||
|
||||
### Manual Rollback
|
||||
|
||||
Manueller Rollback zu vorherigem Release:
|
||||
|
||||
```bash
|
||||
# 1. List available releases
|
||||
ansible -i inventories/production/hosts.yml web_servers -b -a \
|
||||
"ls -lt /home/deploy/michaelschiemer/releases"
|
||||
|
||||
# 2. Identify target release (z.B. v1.2.2)
|
||||
TARGET_RELEASE="v1.2.2"
|
||||
|
||||
# 3. Manual rollback via Ansible
|
||||
ansible -i inventories/production/hosts.yml web_servers -b -m shell -a "
|
||||
cd /home/deploy/michaelschiemer && \
|
||||
docker compose -f current/docker-compose.yml -f current/docker-compose.production.yml down && \
|
||||
ln -sfn releases/${TARGET_RELEASE} current && \
|
||||
docker compose -f current/docker-compose.yml -f current/docker-compose.production.yml up -d
|
||||
"
|
||||
|
||||
# 4. Verify rollback
|
||||
curl -k https://94.16.110.151/health/summary
|
||||
```
|
||||
|
||||
**Oder**: Erstelle Rollback Playbook:
|
||||
|
||||
```yaml
|
||||
# playbooks/rollback-rsync.yml
|
||||
- name: Manual Rollback to Previous Release
|
||||
hosts: web_servers
|
||||
become: true
|
||||
vars:
|
||||
app_name: michaelschiemer
|
||||
app_user: deploy
|
||||
app_base_path: "/home/{{ app_user }}/{{ app_name }}"
|
||||
target_release: "{{ rollback_target }}" # --extra-vars "rollback_target=v1.2.2"
|
||||
|
||||
tasks:
|
||||
- name: Stop current release
|
||||
command: docker compose down
|
||||
args:
|
||||
chdir: "{{ app_base_path }}/current"
|
||||
become_user: "{{ app_user }}"
|
||||
|
||||
- name: Switch to target release
|
||||
file:
|
||||
src: "{{ app_base_path }}/releases/{{ target_release }}"
|
||||
dest: "{{ app_base_path }}/current"
|
||||
state: link
|
||||
force: yes
|
||||
|
||||
- name: Start target release
|
||||
command: docker compose -f docker-compose.yml -f docker-compose.production.yml up -d
|
||||
args:
|
||||
chdir: "{{ app_base_path }}/current"
|
||||
become_user: "{{ app_user }}"
|
||||
|
||||
- name: Health check
|
||||
uri:
|
||||
url: "https://{{ ansible_host }}/health/summary"
|
||||
method: GET
|
||||
status_code: 200
|
||||
validate_certs: no
|
||||
retries: 3
|
||||
delay: 5
|
||||
|
||||
# Usage:
|
||||
# ansible-playbook -i inventories/production/hosts.yml playbooks/rollback-rsync.yml --extra-vars "rollback_target=v1.2.2"
|
||||
```
|
||||
|
||||
## Health Checks
|
||||
|
||||
### Configured Health Endpoints
|
||||
|
||||
**Primary Health Check**: `https://{{ ansible_host }}/health/summary`
|
||||
|
||||
**Retry Strategy**:
|
||||
- Retries: 3
|
||||
- Delay: 5 seconds
|
||||
- Success: HTTP 200 status code
|
||||
|
||||
### Health Check Flow
|
||||
|
||||
```yaml
|
||||
- name: Health check - Summary endpoint (HTTPS)
|
||||
uri:
|
||||
url: "https://{{ ansible_host }}/health/summary"
|
||||
method: GET
|
||||
return_content: yes
|
||||
status_code: 200
|
||||
validate_certs: no
|
||||
follow_redirects: none
|
||||
register: health_check
|
||||
retries: 3
|
||||
delay: 5
|
||||
until: health_check.status == 200
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Rollback on health check failure
|
||||
block:
|
||||
- name: Stop failed release containers
|
||||
- name: Switch symlink back to previous release
|
||||
- name: Start previous release containers
|
||||
- name: Remove failed release
|
||||
- name: Log rollback
|
||||
- name: Fail deployment
|
||||
when: health_check.status != 200
|
||||
```
|
||||
|
||||
### Custom Health Endpoints
|
||||
|
||||
Füge weitere Health Checks hinzu:
|
||||
|
||||
```yaml
|
||||
# Nach der Primary Health Check in deploy-rsync-based.yml
|
||||
- name: Health check - Database connectivity
|
||||
uri:
|
||||
url: "https://{{ ansible_host }}/health/database"
|
||||
method: GET
|
||||
status_code: 200
|
||||
validate_certs: no
|
||||
retries: 2
|
||||
delay: 3
|
||||
ignore_errors: yes
|
||||
register: db_health_check
|
||||
|
||||
- name: Health check - Cache service
|
||||
uri:
|
||||
url: "https://{{ ansible_host }}/health/cache"
|
||||
method: GET
|
||||
status_code: 200
|
||||
validate_certs: no
|
||||
retries: 2
|
||||
delay: 3
|
||||
ignore_errors: yes
|
||||
register: cache_health_check
|
||||
|
||||
- name: Aggregate health check results
|
||||
set_fact:
|
||||
overall_health: "{{ health_check.status == 200 and db_health_check.status == 200 and cache_health_check.status == 200 }}"
|
||||
|
||||
- name: Rollback on any health check failure
|
||||
block:
|
||||
# ... rollback steps ...
|
||||
when: not overall_health
|
||||
```
|
||||
|
||||
## Monitoring & Logging
|
||||
|
||||
### Deployment Log
|
||||
|
||||
Alle Deployments werden geloggt:
|
||||
|
||||
```bash
|
||||
# Deployment log anzeigen
|
||||
ssh deploy@94.16.110.151 'tail -50 /home/deploy/michaelschiemer/deploy.log'
|
||||
```
|
||||
|
||||
**Log Format**:
|
||||
```
|
||||
[2024-10-26T18:50:30Z] Deployment started - Release: v1.2.3 - User: michael
|
||||
[2024-10-26T18:50:35Z] Release: v1.2.3 | Git Hash: a1b2c3d | Commit: a1b2c3d4e5f6g7h8i9j0
|
||||
[2024-10-26T18:50:50Z] Symlink switched: /home/deploy/michaelschiemer/current -> releases/v1.2.3
|
||||
[2024-10-26T18:50:55Z] Health check: 200
|
||||
[2024-10-26T18:50:56Z] Cleanup: Kept 5 releases, removed 1
|
||||
[2024-10-26T18:50:57Z] Deployment completed successfully - Release: v1.2.3
|
||||
```
|
||||
|
||||
### Docker Logs
|
||||
|
||||
```bash
|
||||
# Application logs
|
||||
ssh deploy@94.16.110.151 'cd /home/deploy/michaelschiemer/current && docker compose logs -f'
|
||||
|
||||
# Specific service
|
||||
ssh deploy@94.16.110.151 'cd /home/deploy/michaelschiemer/current && docker compose logs -f php'
|
||||
```
|
||||
|
||||
### System Monitoring
|
||||
|
||||
```bash
|
||||
# Disk usage
|
||||
ansible -i inventories/production/hosts.yml web_servers -b -a "df -h /home/deploy/michaelschiemer"
|
||||
|
||||
# Release directory sizes
|
||||
ansible -i inventories/production/hosts.yml web_servers -b -a "du -sh /home/deploy/michaelschiemer/releases/*"
|
||||
|
||||
# Container status
|
||||
ansible -i inventories/production/hosts.yml web_servers -b -a "docker ps"
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Shared Directories
|
||||
|
||||
Konfiguriert in `deploy-rsync-based.yml`:
|
||||
|
||||
```yaml
|
||||
shared_dirs:
|
||||
- storage/sessions
|
||||
- public/uploads
|
||||
|
||||
shared_files:
|
||||
- .env.production
|
||||
```
|
||||
|
||||
**Hinweis**: `storage/logs`, `storage/cache`, `storage/uploads` werden via Docker Volumes verwaltet.
|
||||
|
||||
### Rsync Exclusions
|
||||
|
||||
Files/Directories die NICHT deployiert werden:
|
||||
|
||||
```yaml
|
||||
rsync_excludes:
|
||||
- .git/
|
||||
- .github/
|
||||
- node_modules/
|
||||
- .env
|
||||
- .env.local
|
||||
- .env.development
|
||||
- storage/
|
||||
- public/uploads/
|
||||
- tests/
|
||||
- .idea/
|
||||
- .vscode/
|
||||
- "*.log"
|
||||
- .DS_Store
|
||||
- deployment/
|
||||
- database.sqlite
|
||||
- "*.cache"
|
||||
- .php-cs-fixer.cache
|
||||
- var/cache/
|
||||
- var/logs/
|
||||
```
|
||||
|
||||
### Keep Releases
|
||||
|
||||
Anzahl der beibehaltenen Releases:
|
||||
|
||||
```yaml
|
||||
keep_releases: 5 # Standard: 5 Releases
|
||||
```
|
||||
|
||||
Ändere nach Bedarf:
|
||||
```bash
|
||||
ansible-playbook ... --extra-vars "keep_releases=10"
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Problem: Deployment Lock existiert
|
||||
|
||||
**Error**:
|
||||
```
|
||||
FAILED! => msg: Deployment already in progress. Lock file exists: /home/deploy/michaelschiemer/.deploy.lock
|
||||
```
|
||||
|
||||
**Ursache**: Vorheriges Deployment wurde unterbrochen
|
||||
|
||||
**Lösung**:
|
||||
```bash
|
||||
# Option 1: Force deployment
|
||||
ansible-playbook ... --extra-vars "force_deploy=true"
|
||||
|
||||
# Option 2: Lock manuell entfernen
|
||||
ansible -i inventories/production/hosts.yml web_servers -b -m file \
|
||||
-a "path=/home/deploy/michaelschiemer/.deploy.lock state=absent"
|
||||
```
|
||||
|
||||
### Problem: Health Check schlägt fehl
|
||||
|
||||
**Error**:
|
||||
```
|
||||
FAILED! => Deployment failed - health check returned 503. Rolled back to previous release.
|
||||
```
|
||||
|
||||
**Diagnose**:
|
||||
```bash
|
||||
# 1. Check application logs
|
||||
ssh deploy@94.16.110.151 'cd /home/deploy/michaelschiemer/current && docker compose logs --tail=100'
|
||||
|
||||
# 2. Check container status
|
||||
ssh deploy@94.16.110.151 'docker ps -a'
|
||||
|
||||
# 3. Manual health check
|
||||
curl -k -v https://94.16.110.151/health/summary
|
||||
|
||||
# 4. Check deployment log
|
||||
ssh deploy@94.16.110.151 'tail -100 /home/deploy/michaelschiemer/deploy.log'
|
||||
```
|
||||
|
||||
**Häufige Ursachen**:
|
||||
- .env.production fehlt oder fehlerhaft
|
||||
- Database migration fehlgeschlagen
|
||||
- Docker container starten nicht
|
||||
- SSL Zertifikat Probleme
|
||||
|
||||
### Problem: Rsync zu langsam
|
||||
|
||||
**Symptom**: Deployment dauert mehrere Minuten
|
||||
|
||||
**Optimierung**:
|
||||
```yaml
|
||||
# In deploy-rsync-based.yml - rsync command erweitern
|
||||
--compress # Kompression aktiviert
|
||||
--delete-after # Löschen nach Transfer
|
||||
--delay-updates # Atomic updates
|
||||
```
|
||||
|
||||
**Alternative**: Rsync via lokales Netzwerk statt Internet:
|
||||
```yaml
|
||||
# Wenn Production Server im gleichen Netzwerk
|
||||
ansible_host: 192.168.1.100 # Lokale IP statt öffentliche
|
||||
```
|
||||
|
||||
### Problem: Git Tag nicht erkannt
|
||||
|
||||
**Symptom**: Deployment verwendet Timestamp statt Git Tag
|
||||
|
||||
**Diagnose**:
|
||||
```bash
|
||||
# Check ob auf getaggtem Commit
|
||||
git describe --tags --exact-match
|
||||
# Sollte: v1.2.3 (ohne Fehler)
|
||||
|
||||
# Check ob Tag existiert
|
||||
git tag -l
|
||||
```
|
||||
|
||||
**Lösung**:
|
||||
```bash
|
||||
# 1. Tag erstellen falls fehlend
|
||||
git tag v1.2.3
|
||||
git push origin v1.2.3
|
||||
|
||||
# 2. Oder Tag explizit angeben
|
||||
ansible-playbook ... --extra-vars "release_tag=v1.2.3"
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Always Tag Releases
|
||||
|
||||
```bash
|
||||
# Vor Production Deployment immer Git Tag erstellen
|
||||
git tag -a v1.2.3 -m "Release v1.2.3: Feature description"
|
||||
git push origin v1.2.3
|
||||
```
|
||||
|
||||
**Vorteile**:
|
||||
- Klare Release-Historie
|
||||
- Einfaches Rollback zu spezifischen Versionen
|
||||
- Semantic Versioning tracking
|
||||
|
||||
### 2. Test Deployment in Staging First
|
||||
|
||||
```bash
|
||||
# Staging deployment (separate inventory)
|
||||
ansible-playbook -i inventories/staging/hosts.yml playbooks/deploy-rsync-based.yml \
|
||||
--extra-vars "release_tag=v1.2.3"
|
||||
|
||||
# Nach erfolgreichen Tests → Production
|
||||
ansible-playbook -i inventories/production/hosts.yml playbooks/deploy-rsync-based.yml \
|
||||
--extra-vars "release_tag=v1.2.3"
|
||||
```
|
||||
|
||||
### 3. Monitor Deployment Log
|
||||
|
||||
```bash
|
||||
# Real-time deployment monitoring
|
||||
ssh deploy@94.16.110.151 'tail -f /home/deploy/michaelschiemer/deploy.log'
|
||||
```
|
||||
|
||||
### 4. Backup vor Major Releases
|
||||
|
||||
```bash
|
||||
# Database backup vor Major Release
|
||||
ssh deploy@94.16.110.151 'cd /home/deploy/michaelschiemer/current && \
|
||||
docker compose exec php php console.php db:backup'
|
||||
```
|
||||
|
||||
### 5. Verify Health Before Release Tag
|
||||
|
||||
```bash
|
||||
# Health check auf Staging
|
||||
curl -k https://staging.michaelschiemer.de/health/summary
|
||||
|
||||
# Bei Erfolg → Production Tag
|
||||
git tag v1.2.3
|
||||
git push origin v1.2.3
|
||||
```
|
||||
|
||||
## Comparison: Rsync vs Git-based
|
||||
|
||||
### Rsync-based (Current)
|
||||
|
||||
**Vorteile**:
|
||||
- ✅ Keine Git-Server Abhängigkeit
|
||||
- ✅ Funktioniert mit lokalem Development
|
||||
- ✅ Schnell für kleine Änderungen
|
||||
- ✅ Einfaches Setup
|
||||
- ✅ Git Tag Support ohne External Server
|
||||
|
||||
**Nachteile**:
|
||||
- ❌ Keine Git-Historie auf Production Server
|
||||
- ❌ Erfordert lokale Build-Steps (Composer, NPM)
|
||||
- ❌ Rsync über Internet kann langsam sein
|
||||
|
||||
### Git-based
|
||||
|
||||
**Vorteile**:
|
||||
- ✅ Git-Historie auf Production Server
|
||||
- ✅ Atomare Releases mit Git Commits
|
||||
- ✅ Build direkt auf Production Server
|
||||
- ✅ Kein lokales Build erforderlich
|
||||
|
||||
**Nachteile**:
|
||||
- ❌ Gitea Server muss öffentlich erreichbar sein
|
||||
- ❌ Zusätzliche Infrastruktur (Gitea)
|
||||
- ❌ SSH Key Management komplexer
|
||||
|
||||
## Performance Optimizations
|
||||
|
||||
### 1. Pre-built Assets
|
||||
|
||||
Assets werden lokal gebaut → schnelleres Deployment:
|
||||
```yaml
|
||||
pre_tasks:
|
||||
- name: Install Composer dependencies locally
|
||||
- name: Build NPM assets locally
|
||||
```
|
||||
|
||||
### 2. Docker Layer Caching
|
||||
|
||||
Docker Images werden auf Production Server gecached → schnellerer Start.
|
||||
|
||||
### 3. Shared Directories
|
||||
|
||||
Shared directories vermeiden unnötiges Kopieren:
|
||||
- `storage/sessions`
|
||||
- `public/uploads`
|
||||
- `.env.production`
|
||||
|
||||
### 4. Cleanup Old Releases
|
||||
|
||||
Nur 5 Releases behalten → spart Disk Space:
|
||||
```yaml
|
||||
keep_releases: 5
|
||||
```
|
||||
|
||||
## Related Files
|
||||
|
||||
- `deploy-rsync-based.yml` - Rsync-based Deployment Playbook
|
||||
- `deploy-git-based.yml` - Git-based Deployment Playbook (Alternative)
|
||||
- `rollback-git-based.yml` - Git-based Rollback Playbook
|
||||
- `inventories/production/hosts.yml` - Production Server Configuration
|
||||
|
||||
## Zusammenfassung
|
||||
|
||||
Das rsync-based Deployment bietet:
|
||||
- ✅ **Production-Ready** Zero-Downtime Deployment
|
||||
- ✅ **Git Tag Support** für klare Release-Historie
|
||||
- ✅ **Automatischer Rollback** bei Failures
|
||||
- ✅ **Einfaches Setup** ohne externe Dependencies
|
||||
- ✅ **Schnell und Zuverlässig** für Development und Production
|
||||
|
||||
**Empfehlung**: Ideal für lokale Development → Production Workflows ohne zusätzliche Git-Server-Infrastruktur.
|
||||
@@ -1,6 +1,11 @@
|
||||
---
|
||||
# Git-based Deployment Playbook with Releases/Symlink Pattern
|
||||
# Git-based Deployment Playbook with Releases/Symlink Pattern (Gitea)
|
||||
# Implements production-ready deployment with zero-downtime and rollback support
|
||||
# Uses Gitea as Git repository server with SSH-based authentication
|
||||
#
|
||||
# Prerequisites:
|
||||
# - SSH deploy key must be placed in deployment/infrastructure/secrets/gitea_deploy_key
|
||||
# - Deploy key must be added to Gitea repository or user account
|
||||
#
|
||||
# Usage:
|
||||
# ansible-playbook -i inventories/production/hosts.yml playbooks/deploy-git-based.yml
|
||||
@@ -23,9 +28,11 @@
|
||||
shared_path: "{{ app_base_path }}/shared"
|
||||
current_path: "{{ app_base_path }}/current"
|
||||
|
||||
# Git configuration
|
||||
git_repo: "https://github.com/michaelschiemer/michaelschiemer.git"
|
||||
# Git configuration (Gitea)
|
||||
# Use localhost for local testing, git.michaelschiemer.de for production
|
||||
git_repo: "git@localhost:michael/michaelschiemer.git"
|
||||
git_branch: "{{ release_tag | default('main') }}"
|
||||
git_ssh_key: "/home/{{ app_user }}/.ssh/gitea_deploy_key"
|
||||
|
||||
# Release configuration
|
||||
release_timestamp: "{{ ansible_date_time.epoch }}"
|
||||
@@ -47,7 +54,72 @@
|
||||
shared_files:
|
||||
- .env.production
|
||||
|
||||
pre_tasks:
|
||||
tasks:
|
||||
# ==========================================
|
||||
# 1. SSH Key Setup for Gitea Access
|
||||
# ==========================================
|
||||
|
||||
- name: Create .ssh directory for deploy user
|
||||
file:
|
||||
path: "/home/{{ app_user }}/.ssh"
|
||||
state: directory
|
||||
owner: "{{ app_user }}"
|
||||
group: "{{ app_group }}"
|
||||
mode: '0700'
|
||||
|
||||
- name: Copy Gitea deploy SSH private key
|
||||
copy:
|
||||
src: "{{ playbook_dir }}/../secrets/gitea_deploy_key"
|
||||
dest: "{{ git_ssh_key }}"
|
||||
owner: "{{ app_user }}"
|
||||
group: "{{ app_group }}"
|
||||
mode: '0600'
|
||||
|
||||
- name: Copy Gitea deploy SSH public key
|
||||
copy:
|
||||
src: "{{ playbook_dir }}/../secrets/gitea_deploy_key.pub"
|
||||
dest: "{{ git_ssh_key }}.pub"
|
||||
owner: "{{ app_user }}"
|
||||
group: "{{ app_group }}"
|
||||
mode: '0644'
|
||||
|
||||
- name: Configure SSH for Gitea (disable StrictHostKeyChecking)
|
||||
blockinfile:
|
||||
path: "/home/{{ app_user }}/.ssh/config"
|
||||
create: yes
|
||||
owner: "{{ app_user }}"
|
||||
group: "{{ app_group }}"
|
||||
mode: '0600'
|
||||
marker: "# {mark} ANSIBLE MANAGED BLOCK - Gitea SSH Config"
|
||||
block: |
|
||||
Host localhost
|
||||
HostName localhost
|
||||
Port 2222
|
||||
User git
|
||||
IdentityFile {{ git_ssh_key }}
|
||||
StrictHostKeyChecking no
|
||||
UserKnownHostsFile /dev/null
|
||||
|
||||
Host git.michaelschiemer.de
|
||||
HostName git.michaelschiemer.de
|
||||
Port 2222
|
||||
User git
|
||||
IdentityFile {{ git_ssh_key }}
|
||||
StrictHostKeyChecking no
|
||||
UserKnownHostsFile /dev/null
|
||||
|
||||
# ==========================================
|
||||
# 2. Directory Structure Setup
|
||||
# ==========================================
|
||||
|
||||
- name: Create base application directory
|
||||
file:
|
||||
path: "{{ app_base_path }}"
|
||||
state: directory
|
||||
owner: "{{ app_user }}"
|
||||
group: "{{ app_group }}"
|
||||
mode: '0755'
|
||||
|
||||
- name: Check if deployment lock exists
|
||||
stat:
|
||||
path: "{{ app_base_path }}/.deploy.lock"
|
||||
@@ -74,19 +146,6 @@
|
||||
owner: "{{ app_user }}"
|
||||
group: "{{ app_group }}"
|
||||
|
||||
tasks:
|
||||
# ==========================================
|
||||
# 1. Directory Structure Setup
|
||||
# ==========================================
|
||||
|
||||
- name: Create base application directory
|
||||
file:
|
||||
path: "{{ app_base_path }}"
|
||||
state: directory
|
||||
owner: "{{ app_user }}"
|
||||
group: "{{ app_group }}"
|
||||
mode: '0755'
|
||||
|
||||
- name: Create releases directory
|
||||
file:
|
||||
path: "{{ releases_path }}"
|
||||
|
||||
@@ -28,7 +28,8 @@
|
||||
|
||||
# Release configuration
|
||||
release_timestamp: "{{ ansible_date_time.epoch }}"
|
||||
release_name: "{{ release_tag | default(release_timestamp) }}"
|
||||
# Note: effective_release_tag is set in pre_tasks based on Git tags
|
||||
release_name: "{{ effective_release_tag | default(release_tag | default(release_timestamp)) }}"
|
||||
release_path: "{{ releases_path }}/{{ release_name }}"
|
||||
|
||||
# Deployment settings
|
||||
@@ -66,8 +67,46 @@
|
||||
- .php-cs-fixer.cache
|
||||
- var/cache/
|
||||
- var/logs/
|
||||
- "*.php85/"
|
||||
- src/**/*.php85/
|
||||
|
||||
pre_tasks:
|
||||
# Git Tag Detection and Validation
|
||||
- name: Get current Git tag (if release_tag not specified)
|
||||
local_action:
|
||||
module: command
|
||||
cmd: git describe --tags --exact-match
|
||||
chdir: "{{ local_project_path }}"
|
||||
register: git_current_tag
|
||||
become: false
|
||||
ignore_errors: yes
|
||||
when: release_tag is not defined
|
||||
|
||||
- name: Get current Git commit hash
|
||||
local_action:
|
||||
module: command
|
||||
cmd: git rev-parse --short HEAD
|
||||
chdir: "{{ local_project_path }}"
|
||||
register: git_commit_hash
|
||||
become: false
|
||||
|
||||
- name: Set release_name from Git tag or timestamp
|
||||
set_fact:
|
||||
effective_release_tag: "{{ release_tag | default(git_current_tag.stdout if (git_current_tag is defined and git_current_tag.rc == 0) else release_timestamp) }}"
|
||||
git_hash: "{{ git_commit_hash.stdout }}"
|
||||
|
||||
- name: Display deployment information
|
||||
debug:
|
||||
msg:
|
||||
- "=========================================="
|
||||
- "Deployment Information"
|
||||
- "=========================================="
|
||||
- "Release: {{ effective_release_tag }}"
|
||||
- "Git Hash: {{ git_hash }}"
|
||||
- "Source: {{ local_project_path }}"
|
||||
- "Target: {{ ansible_host }}"
|
||||
- "=========================================="
|
||||
|
||||
- name: Install Composer dependencies locally before deployment
|
||||
local_action:
|
||||
module: command
|
||||
@@ -155,6 +194,11 @@
|
||||
# 2. Rsync Application Code to New Release
|
||||
# ==========================================
|
||||
|
||||
- name: Remove old release directory if exists (prevent permission issues)
|
||||
file:
|
||||
path: "{{ release_path }}"
|
||||
state: absent
|
||||
|
||||
- name: Create new release directory
|
||||
file:
|
||||
path: "{{ release_path }}"
|
||||
@@ -163,16 +207,25 @@
|
||||
group: "{{ app_group }}"
|
||||
mode: '0755'
|
||||
|
||||
- name: Sync application code to new release via rsync
|
||||
synchronize:
|
||||
src: "{{ local_project_path }}/"
|
||||
dest: "{{ release_path }}/"
|
||||
delete: yes
|
||||
recursive: yes
|
||||
rsync_opts: "{{ rsync_excludes | map('regex_replace', '^(.*)$', '--exclude=\\1') | list }}"
|
||||
private_key: "{{ ansible_ssh_private_key_file }}"
|
||||
- name: Temporarily rename .dockerignore to prevent rsync -F from reading it
|
||||
command: mv {{ local_project_path }}/.dockerignore {{ local_project_path }}/.dockerignore.bak
|
||||
delegate_to: localhost
|
||||
become: false
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Sync application code to new release via rsync (raw command to avoid -F flag)
|
||||
command: >
|
||||
rsync --delay-updates --compress --delete-after --archive --rsh='ssh -i {{ ansible_ssh_private_key_file }} -o StrictHostKeyChecking=no' --no-g --no-o
|
||||
{% for exclude in rsync_excludes %}--exclude='{{ exclude }}' {% endfor %}
|
||||
{{ local_project_path }}/ {{ app_user }}@{{ ansible_host }}:{{ release_path }}/
|
||||
delegate_to: localhost
|
||||
become: false
|
||||
|
||||
- name: Restore .dockerignore after rsync
|
||||
command: mv {{ local_project_path }}/.dockerignore.bak {{ local_project_path }}/.dockerignore
|
||||
delegate_to: localhost
|
||||
become: false
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Set correct ownership for release
|
||||
file:
|
||||
@@ -191,10 +244,10 @@
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Log commit hash
|
||||
- name: Log release and commit information
|
||||
lineinfile:
|
||||
path: "{{ app_base_path }}/deploy.log"
|
||||
line: "[{{ ansible_date_time.iso8601 }}] Commit: {{ commit_hash.stdout | default('N/A - not a git repository') }}"
|
||||
line: "[{{ ansible_date_time.iso8601 }}] Release: {{ effective_release_tag }} | Git Hash: {{ git_hash | default('N/A') }} | Commit: {{ commit_hash.stdout | default('N/A') }}"
|
||||
when: commit_hash.rc == 0
|
||||
|
||||
# ==========================================
|
||||
@@ -325,6 +378,29 @@
|
||||
path: "{{ app_base_path }}/deploy.log"
|
||||
line: "[{{ ansible_date_time.iso8601 }}] Symlink switched: {{ current_path }} -> {{ release_path }}"
|
||||
|
||||
# ==========================================
|
||||
# 8.5. SSL Certificate Setup
|
||||
# ==========================================
|
||||
|
||||
- name: Create SSL directory in release
|
||||
file:
|
||||
path: "{{ release_path }}/ssl"
|
||||
state: directory
|
||||
owner: "{{ app_user }}"
|
||||
group: "{{ app_group }}"
|
||||
mode: '0755'
|
||||
|
||||
- name: Copy SSL certificates from certbot to release (if they exist)
|
||||
shell: |
|
||||
if docker ps | grep -q certbot; then
|
||||
docker cp certbot:/etc/letsencrypt/archive/michaelschiemer.de/fullchain1.pem {{ release_path }}/ssl/fullchain.pem 2>/dev/null || true
|
||||
docker cp certbot:/etc/letsencrypt/archive/michaelschiemer.de/privkey1.pem {{ release_path }}/ssl/privkey.pem 2>/dev/null || true
|
||||
chown {{ app_user }}:{{ app_group }} {{ release_path }}/ssl/*.pem 2>/dev/null || true
|
||||
fi
|
||||
args:
|
||||
chdir: "{{ current_path }}"
|
||||
ignore_errors: yes
|
||||
|
||||
# ==========================================
|
||||
# 9. Start Docker Containers
|
||||
# ==========================================
|
||||
@@ -344,16 +420,17 @@
|
||||
# ==========================================
|
||||
|
||||
- name: Wait for application to be ready
|
||||
wait_for:
|
||||
timeout: 10
|
||||
delegate_to: localhost
|
||||
pause:
|
||||
seconds: 10
|
||||
|
||||
- name: Health check - Summary endpoint
|
||||
- name: Health check - Summary endpoint (HTTPS)
|
||||
uri:
|
||||
url: "http://{{ ansible_host }}/health/summary"
|
||||
url: "https://{{ ansible_host }}/health/summary"
|
||||
method: GET
|
||||
return_content: yes
|
||||
status_code: 200
|
||||
validate_certs: no
|
||||
follow_redirects: none
|
||||
register: health_check
|
||||
retries: 3
|
||||
delay: 5
|
||||
|
||||
3
deployment/infrastructure/secrets/.gitignore
vendored
Normal file
3
deployment/infrastructure/secrets/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# SECURITY: Never commit SSH keys or secrets to version control!
|
||||
*
|
||||
!.gitignore
|
||||
Reference in New Issue
Block a user