Fix cache directory paths for production deployment
- Change FileCache CACHE_PATH from relative to absolute path - Change FileCacheCleaner cache folder to absolute path - Resolves read-only file system issue in production containers - Cache now uses writable /var/www/html/storage/cache location
This commit is contained in:
2
deployment/infrastructure/.gitignore
vendored
Normal file
2
deployment/infrastructure/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
# Ignore local Ansible vault pass in infrastructure directory
|
||||
.vault_pass
|
||||
226
deployment/infrastructure/playbooks/deploy-application.yml
Normal file
226
deployment/infrastructure/playbooks/deploy-application.yml
Normal file
@@ -0,0 +1,226 @@
|
||||
---
|
||||
# Production Container Deployment Playbook
|
||||
# Deploys pre-built container images for Custom PHP Framework
|
||||
|
||||
- name: Deploy Custom PHP Framework Application
|
||||
hosts: web_servers
|
||||
become: true
|
||||
gather_facts: true
|
||||
|
||||
vars:
|
||||
app_path: "/var/www/html"
|
||||
backup_path: "/var/www/backups"
|
||||
image_tag: "{{ IMAGE_TAG | default('latest') }}"
|
||||
domain_name: "{{ DOMAIN_NAME | default('michaelschiemer.de') }}"
|
||||
backup_enabled: "{{ BACKUP_ENABLED | default(true) | bool }}"
|
||||
backup_retention_days: "{{ BACKUP_RETENTION_DAYS | default(30) }}"
|
||||
cdn_update: "{{ CDN_UPDATE | default(false) | bool }}"
|
||||
# Pfade für Templates/Compose relativ zum Playbook-Verzeichnis
|
||||
compose_base_src: "{{ playbook_dir }}/../../../docker-compose.yml"
|
||||
compose_overlay_src: "{{ playbook_dir }}/../../applications/docker-compose.{{ environment }}.yml"
|
||||
env_template_src: "{{ playbook_dir }}/../../applications/environments/.env.{{ environment }}.template"
|
||||
# Compose-Projektname: Standardmäßig Verzeichnisname von app_path (z. B. 'html')
|
||||
compose_project: "{{ compose_project_name | default(app_path | basename) }}"
|
||||
|
||||
pre_tasks:
|
||||
- name: Verify deployment requirements
|
||||
assert:
|
||||
that:
|
||||
- app_path is defined
|
||||
- domain_name is defined
|
||||
- image_tag is defined
|
||||
- image_tag != 'latest' or environment != 'production'
|
||||
fail_msg: "Production deployment requires specific image tag (not 'latest')"
|
||||
tags: always
|
||||
|
||||
- name: Create required directories
|
||||
ansible.builtin.file:
|
||||
path: "{{ item }}"
|
||||
state: directory
|
||||
owner: deploy
|
||||
group: deploy
|
||||
mode: '0755'
|
||||
loop:
|
||||
- "{{ app_path }}"
|
||||
- "{{ backup_path }}"
|
||||
- /var/log/applications
|
||||
tags: always
|
||||
|
||||
- name: Store current image tag for rollback
|
||||
ansible.builtin.shell: |
|
||||
if [ -f {{ app_path }}/.env.{{ environment }} ]; then
|
||||
grep '^IMAGE_TAG=' {{ app_path }}/.env.{{ environment }} | cut -d'=' -f2 > {{ app_path }}/.last_release || echo 'none'
|
||||
fi
|
||||
ignore_errors: true
|
||||
tags: backup
|
||||
|
||||
tasks:
|
||||
- name: Check for existing deployment
|
||||
ansible.builtin.stat:
|
||||
path: "{{ app_path }}/docker-compose.yml"
|
||||
register: existing_deployment
|
||||
tags: deploy
|
||||
|
||||
- name: Render environment file from template
|
||||
ansible.builtin.template:
|
||||
src: "{{ env_template_src }}"
|
||||
dest: "{{ app_path }}/.env.{{ environment }}"
|
||||
owner: deploy
|
||||
group: deploy
|
||||
mode: '0600'
|
||||
backup: true
|
||||
vars:
|
||||
IMAGE_TAG: "{{ image_tag }}"
|
||||
DOMAIN_NAME: "{{ domain_name }}"
|
||||
no_log: true
|
||||
tags: deploy
|
||||
|
||||
- name: Copy Docker Compose files (base + overlay)
|
||||
ansible.builtin.copy:
|
||||
src: "{{ item.src }}"
|
||||
dest: "{{ app_path }}/{{ item.dest }}"
|
||||
owner: deploy
|
||||
group: deploy
|
||||
mode: '0644'
|
||||
loop:
|
||||
- { src: "{{ compose_base_src }}", dest: "docker-compose.yml" }
|
||||
- { src: "{{ compose_overlay_src }}", dest: "docker-compose.{{ environment }}.yml" }
|
||||
tags: deploy
|
||||
|
||||
- name: Stop existing services gracefully if present
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ app_path }}"
|
||||
files:
|
||||
- docker-compose.yml
|
||||
- "docker-compose.{{ environment }}.yml"
|
||||
env_files:
|
||||
- ".env.{{ environment }}"
|
||||
state: stopped
|
||||
timeout: 60
|
||||
when: existing_deployment.stat.exists
|
||||
ignore_errors: true
|
||||
tags: deploy
|
||||
|
||||
- name: Create storage volumes with proper permissions
|
||||
ansible.builtin.file:
|
||||
path: "{{ app_path }}/{{ item }}"
|
||||
state: directory
|
||||
owner: www-data
|
||||
group: www-data
|
||||
mode: '0775'
|
||||
loop:
|
||||
- storage
|
||||
- storage/logs
|
||||
- storage/cache
|
||||
- var
|
||||
- var/logs
|
||||
tags: deploy
|
||||
|
||||
- name: Deploy application with Docker Compose v2
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ app_path }}"
|
||||
files:
|
||||
- docker-compose.yml
|
||||
- "docker-compose.{{ environment }}.yml"
|
||||
env_files:
|
||||
- ".env.{{ environment }}"
|
||||
pull: true
|
||||
build: false
|
||||
state: present
|
||||
recreate: smart
|
||||
remove_orphans: true
|
||||
timeout: 300
|
||||
tags: deploy
|
||||
|
||||
- name: Wait for PHP container to be healthy (label-based)
|
||||
community.docker.docker_container_info:
|
||||
filters:
|
||||
label:
|
||||
- "com.docker.compose.service=php"
|
||||
- "com.docker.compose.project={{ compose_project }}"
|
||||
register: php_info
|
||||
retries: 20
|
||||
delay: 10
|
||||
until: php_info.containers is defined and
|
||||
(php_info.containers | length) > 0 and
|
||||
(
|
||||
(php_info.containers[0].State.Health is defined and php_info.containers[0].State.Health.Status == "healthy")
|
||||
or
|
||||
php_info.containers[0].State.Status == "running"
|
||||
)
|
||||
tags: deploy
|
||||
|
||||
- name: Run database migrations
|
||||
community.docker.docker_container_exec:
|
||||
container: "{{ php_info.containers[0].Id }}"
|
||||
command: php console.php db:migrate --force
|
||||
chdir: /var/www/html
|
||||
tags: deploy
|
||||
|
||||
- name: Clear application caches
|
||||
community.docker.docker_container_exec:
|
||||
container: "{{ php_info.containers[0].Id }}"
|
||||
command: "php console.php {{ item }}"
|
||||
chdir: /var/www/html
|
||||
loop:
|
||||
- cache:clear
|
||||
- view:clear
|
||||
ignore_errors: true
|
||||
tags: deploy
|
||||
|
||||
- name: Wait for application to be ready
|
||||
ansible.builtin.uri:
|
||||
url: "https://{{ domain_name }}/health"
|
||||
method: GET
|
||||
status_code: 200
|
||||
timeout: 30
|
||||
headers:
|
||||
User-Agent: "Mozilla/5.0 (Ansible Health Check)"
|
||||
validate_certs: true
|
||||
register: http_health
|
||||
retries: 15
|
||||
delay: 10
|
||||
until: http_health.status == 200
|
||||
tags: deploy
|
||||
|
||||
- name: Store successful deployment tag
|
||||
ansible.builtin.copy:
|
||||
content: "{{ image_tag }}"
|
||||
dest: "{{ app_path }}/.last_successful_release"
|
||||
owner: deploy
|
||||
group: deploy
|
||||
mode: '0644'
|
||||
tags: deploy
|
||||
|
||||
post_tasks:
|
||||
- name: Clean up old backups
|
||||
ansible.builtin.find:
|
||||
paths: "{{ backup_path }}"
|
||||
age: "{{ backup_retention_days }}d"
|
||||
file_type: directory
|
||||
register: old_backups
|
||||
when: backup_enabled
|
||||
tags: cleanup
|
||||
|
||||
- name: Remove old backup directories
|
||||
ansible.builtin.file:
|
||||
path: "{{ item.path }}"
|
||||
state: absent
|
||||
loop: "{{ old_backups.files }}"
|
||||
when: backup_enabled and old_backups.files is defined
|
||||
tags: cleanup
|
||||
|
||||
- name: Import CDN update playbook if enabled
|
||||
import_playbook: update-cdn.yml
|
||||
when: cdn_update | default(false) | bool
|
||||
tags: cdn
|
||||
|
||||
- name: Deployment success notification
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
- "Application deployment completed successfully"
|
||||
- "Image Tag: {{ image_tag }}"
|
||||
- "Environment: {{ environment }}"
|
||||
- "Domain: {{ domain_name }}"
|
||||
- "CDN Updated: {{ cdn_update }}"
|
||||
tags: always
|
||||
235
deployment/infrastructure/playbooks/rollback.yml
Normal file
235
deployment/infrastructure/playbooks/rollback.yml
Normal file
@@ -0,0 +1,235 @@
|
||||
---
|
||||
# Application Rollback Playbook
|
||||
# Rolls back to a specific image tag using container deployment
|
||||
---
|
||||
# Rollback Playbook: setzt IMAGE_TAG zurück und recreatet Services
|
||||
|
||||
- name: Rollback Custom PHP Framework Application
|
||||
hosts: web_servers
|
||||
become: true
|
||||
gather_facts: false
|
||||
|
||||
vars:
|
||||
app_path: "/var/www/html"
|
||||
domain_name: "{{ DOMAIN_NAME | default('michaelschiemer.de') }}"
|
||||
rollback_tag: "{{ ROLLBACK_TAG | default('') }}"
|
||||
compose_project: "{{ compose_project_name | default(app_path | basename) }}"
|
||||
|
||||
pre_tasks:
|
||||
- name: Validate ROLLBACK_TAG is provided
|
||||
ansible.builtin.fail:
|
||||
msg: "Setze ROLLBACK_TAG auf ein gültiges Image-Tag."
|
||||
when: rollback_tag | length == 0
|
||||
|
||||
tasks:
|
||||
- name: Ensure environment file exists
|
||||
ansible.builtin.stat:
|
||||
path: "{{ app_path }}/.env.{{ environment }}"
|
||||
register: env_file
|
||||
|
||||
- name: Fail if environment file is missing
|
||||
ansible.builtin.fail:
|
||||
msg: "Environment-Datei fehlt: {{ app_path }}/.env.{{ environment }}"
|
||||
when: not env_file.stat.exists
|
||||
|
||||
- name: Write IMAGE_TAG to env file
|
||||
ansible.builtin.lineinfile:
|
||||
path: "{{ app_path }}/.env.{{ environment }}"
|
||||
regexp: '^IMAGE_TAG='
|
||||
line: "IMAGE_TAG={{ rollback_tag }}"
|
||||
create: no
|
||||
backrefs: false
|
||||
mode: "0600"
|
||||
|
||||
- name: Recreate services with rollback tag
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ app_path }}"
|
||||
files:
|
||||
- docker-compose.yml
|
||||
- "docker-compose.{{ environment }}.yml"
|
||||
env_files:
|
||||
- ".env.{{ environment }}"
|
||||
pull: false
|
||||
build: false
|
||||
state: present
|
||||
recreate: smart
|
||||
remove_orphans: true
|
||||
timeout: 300
|
||||
|
||||
- name: Wait for PHP container to be healthy (label-based)
|
||||
community.docker.docker_container_info:
|
||||
filters:
|
||||
label:
|
||||
- "com.docker.compose.service=php"
|
||||
- "com.docker.compose.project={{ compose_project }}"
|
||||
register: php_info
|
||||
retries: 20
|
||||
delay: 10
|
||||
until: php_info.containers is defined and
|
||||
(php_info.containers | length) > 0 and
|
||||
(
|
||||
(php_info.containers[0].State.Health is defined and php_info.containers[0].State.Health.Status == "healthy")
|
||||
or
|
||||
php_info.containers[0].State.Status == "running"
|
||||
)
|
||||
|
||||
- name: Verify application HTTP health
|
||||
ansible.builtin.uri:
|
||||
url: "https://{{ domain_name }}/health"
|
||||
method: GET
|
||||
status_code: 200
|
||||
timeout: 30
|
||||
validate_certs: true
|
||||
register: http_health
|
||||
retries: 15
|
||||
delay: 10
|
||||
until: http_health.status == 200
|
||||
|
||||
post_tasks:
|
||||
- name: Rollback completed
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
- "Rollback erfolgreich"
|
||||
- "Neues aktives Image-Tag: {{ rollback_tag }}"
|
||||
- name: Rollback Custom PHP Framework Application
|
||||
hosts: web_servers
|
||||
become: true
|
||||
gather_facts: true
|
||||
|
||||
vars:
|
||||
app_path: "/var/www/html"
|
||||
rollback_tag: "{{ ROLLBACK_TAG | mandatory }}"
|
||||
domain_name: "{{ DOMAIN_NAME | default('michaelschiemer.de') }}"
|
||||
environment: "{{ ENV | default('production') }}"
|
||||
|
||||
pre_tasks:
|
||||
- name: Verify rollback requirements
|
||||
assert:
|
||||
that:
|
||||
- rollback_tag is defined
|
||||
- rollback_tag != ''
|
||||
- rollback_tag != 'latest'
|
||||
fail_msg: "Rollback requires specific ROLLBACK_TAG (not 'latest')"
|
||||
tags: always
|
||||
|
||||
- name: Check if target tag exists locally
|
||||
community.docker.docker_image_info:
|
||||
name: "{{ project_name | default('michaelschiemer') }}:{{ rollback_tag }}"
|
||||
register: rollback_image_info
|
||||
ignore_errors: true
|
||||
tags: always
|
||||
|
||||
- name: Pull rollback image if not available locally
|
||||
community.docker.docker_image:
|
||||
name: "{{ project_name | default('michaelschiemer') }}:{{ rollback_tag }}"
|
||||
source: pull
|
||||
force_source: true
|
||||
when: rollback_image_info.images | length == 0
|
||||
tags: always
|
||||
|
||||
- name: Store current deployment for emergency recovery
|
||||
shell: |
|
||||
if [ -f {{ app_path }}/.env.{{ environment }} ]; then
|
||||
grep '^IMAGE_TAG=' {{ app_path }}/.env.{{ environment }} | cut -d'=' -f2 > {{ app_path }}/.emergency_recovery_tag || echo 'none'
|
||||
fi
|
||||
tags: backup
|
||||
|
||||
tasks:
|
||||
- name: Update environment with rollback tag
|
||||
template:
|
||||
src: "{{ environment }}.env.template"
|
||||
dest: "{{ app_path }}/.env.{{ environment }}"
|
||||
owner: deploy
|
||||
group: deploy
|
||||
mode: '0600'
|
||||
backup: true
|
||||
vars:
|
||||
IMAGE_TAG: "{{ rollback_tag }}"
|
||||
DOMAIN_NAME: "{{ domain_name }}"
|
||||
no_log: true
|
||||
tags: rollback
|
||||
|
||||
- name: Stop current services
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ app_path }}"
|
||||
files:
|
||||
- docker-compose.yml
|
||||
- "docker-compose.{{ environment }}.yml"
|
||||
env_files:
|
||||
- ".env.{{ environment }}"
|
||||
state: stopped
|
||||
timeout: 120
|
||||
tags: rollback
|
||||
|
||||
- name: Deploy rollback version
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ app_path }}"
|
||||
files:
|
||||
- docker-compose.yml
|
||||
- "docker-compose.{{ environment }}.yml"
|
||||
env_files:
|
||||
- ".env.{{ environment }}"
|
||||
pull: "never" # Use local image
|
||||
build: "never"
|
||||
state: present
|
||||
recreate: "always" # Force recreate for rollback
|
||||
timeout: 300
|
||||
tags: rollback
|
||||
|
||||
- name: Wait for containers to be healthy after rollback
|
||||
community.docker.docker_container_info:
|
||||
name: "{{ item }}"
|
||||
register: container_info
|
||||
retries: 15
|
||||
delay: 10
|
||||
until: container_info.container.State.Health.Status == "healthy" or container_info.container.State.Status == "running"
|
||||
loop:
|
||||
- "{{ ansible_hostname }}_php_1"
|
||||
- "{{ ansible_hostname }}_web_1"
|
||||
- "{{ ansible_hostname }}_db_1"
|
||||
- "{{ ansible_hostname }}_redis_1"
|
||||
ignore_errors: true
|
||||
tags: rollback
|
||||
|
||||
- name: Verify application health after rollback
|
||||
uri:
|
||||
url: "https://{{ domain_name }}/health"
|
||||
method: GET
|
||||
status_code: 200
|
||||
timeout: 30
|
||||
headers:
|
||||
User-Agent: "Mozilla/5.0 (Ansible Rollback Check)"
|
||||
validate_certs: true
|
||||
retries: 10
|
||||
delay: 15
|
||||
tags: rollback
|
||||
|
||||
- name: Update successful rollback tag
|
||||
copy:
|
||||
content: "{{ rollback_tag }}"
|
||||
dest: "{{ app_path }}/.last_successful_release"
|
||||
owner: deploy
|
||||
group: deploy
|
||||
mode: '0644'
|
||||
tags: rollback
|
||||
|
||||
post_tasks:
|
||||
- name: Rollback success notification
|
||||
debug:
|
||||
msg:
|
||||
- "Application rollback completed successfully"
|
||||
- "Rolled back to: {{ rollback_tag }}"
|
||||
- "Environment: {{ environment }}"
|
||||
- "Domain: {{ domain_name }}"
|
||||
- "Emergency recovery tag stored for further rollback if needed"
|
||||
tags: always
|
||||
|
||||
- name: Log rollback event
|
||||
lineinfile:
|
||||
path: "{{ app_path }}/rollback.log"
|
||||
line: "{{ ansible_date_time.iso8601 }} - Rollback to {{ rollback_tag }} from {{ environment }} completed successfully"
|
||||
create: true
|
||||
owner: deploy
|
||||
group: deploy
|
||||
mode: '0644'
|
||||
tags: always
|
||||
@@ -14,7 +14,7 @@ use App\Framework\Filesystem\Storage;
|
||||
|
||||
final readonly class FileCache implements CacheDriver, Scannable
|
||||
{
|
||||
private const string CACHE_PATH = __DIR__ . '/../storage/cache';
|
||||
private const string CACHE_PATH = '/var/www/html/storage/cache';
|
||||
|
||||
public function __construct(
|
||||
private Storage $fileSystem = new FileStorage(),
|
||||
|
||||
@@ -11,7 +11,7 @@ final readonly class FileCacheCleaner
|
||||
{
|
||||
public function __construct(
|
||||
private Storage $fileSystem = new FileStorage(),
|
||||
private string $cacheFolder = __DIR__.'/storage/cache/'
|
||||
private string $cacheFolder = '/var/www/html/storage/cache/'
|
||||
) {
|
||||
}
|
||||
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"error": true,
|
||||
"code": 500,
|
||||
"message": "App\\Framework\\Http\\HttpResponse::__construct(): Argument #1 ($status) must be of type App\\Framework\\Http\\Status, int given, called in /var/www/html/src/Application/Http/Controllers/QrCodeController.php on line 143",
|
||||
"requestId": {},
|
||||
"timestamp": "2025-08-09T17:48:38+00:00",
|
||||
"debug": {
|
||||
"operation": null,
|
||||
"component": null,
|
||||
"exception_class": "TypeError",
|
||||
"client_ip": "172.20.0.1",
|
||||
"request_method": "GET",
|
||||
"request_uri": "https://localhost/api/qrcode/totp?uri=otpauth%3A%2F%2Ftotp%2FTestApp%3Atestuser%40example.com%3Fsecret%3DMFRGGZDFMZTWQ2LK&issuer=TestApp",
|
||||
"user_agent": {
|
||||
"value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"
|
||||
},
|
||||
"memory_usage": "6 MB",
|
||||
"execution_time": 0.058526039123535156,
|
||||
"exception_data": {
|
||||
"exception_message": "App\\Framework\\Http\\HttpResponse::__construct(): Argument #1 ($status) must be of type App\\Framework\\Http\\Status, int given, called in /var/www/html/src/Application/Http/Controllers/QrCodeController.php on line 143",
|
||||
"exception_file": "/var/www/html/src/Framework/Http/HttpResponse.php",
|
||||
"exception_line": 9,
|
||||
"original_exception": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user