feat(deployment): update semaphore configuration and deployment workflows

This commit is contained in:
2025-11-02 20:46:18 +01:00
parent 24cbbccf4c
commit a5cd49bde7
8 changed files with 109 additions and 41 deletions

View File

@@ -820,7 +820,7 @@ jobs:
needs: [changes, build, runtime-base] needs: [changes, build, runtime-base]
if: | if: |
(github.ref_name == 'staging' || github.head_ref == 'staging' || (github.ref_name == '' && contains(github.ref, 'staging'))) && (github.ref_name == 'staging' || github.head_ref == 'staging' || (github.ref_name == '' && contains(github.ref, 'staging'))) &&
(needs.build.result == 'success' || needs.build.result == 'skipped') (needs.build.result != 'failure')
runs-on: ubuntu-latest runs-on: ubuntu-latest
environment: environment:
name: staging name: staging

View File

@@ -50,16 +50,21 @@
group: "{{ ansible_user }}" group: "{{ ansible_user }}"
mode: '0755' mode: '0755'
- name: Determine application environment
set_fact:
application_environment: "{{ APP_ENV | default('production') }}"
application_compose_suffix: "{{ 'staging.yml' if application_environment == 'staging' else 'production.yml' }}"
- name: Check if docker-compose.base.yml exists in application stack - name: Check if docker-compose.base.yml exists in application stack
stat: stat:
path: "{{ app_stack_path }}/docker-compose.base.yml" path: "{{ app_stack_path }}/docker-compose.base.yml"
register: compose_base_exists register: compose_base_exists
when: not (application_sync_files | default(false) | bool) when: not (application_sync_files | default(false) | bool)
- name: Check if docker-compose.production.yml exists in application stack - name: Check if docker-compose override file exists in application stack (production or staging)
stat: stat:
path: "{{ app_stack_path }}/docker-compose.production.yml" path: "{{ app_stack_path }}/docker-compose.{{ application_compose_suffix }}"
register: compose_prod_exists register: compose_override_exists
when: not (application_sync_files | default(false) | bool) when: not (application_sync_files | default(false) | bool)
- name: Fail if docker-compose files don't exist - name: Fail if docker-compose files don't exist
@@ -69,7 +74,7 @@
Required files: Required files:
- docker-compose.base.yml - docker-compose.base.yml
- docker-compose.production.yml - docker-compose.{{ application_compose_suffix }}
The Application Stack must be deployed first via: The Application Stack must be deployed first via:
ansible-playbook -i inventory/production.yml playbooks/setup-infrastructure.yml ansible-playbook -i inventory/production.yml playbooks/setup-infrastructure.yml
@@ -77,7 +82,7 @@
This will create the application stack with docker-compose files and .env file. This will create the application stack with docker-compose files and .env file.
when: when:
- not (application_sync_files | default(false) | bool) - not (application_sync_files | default(false) | bool)
- (not compose_base_exists.stat.exists or not compose_prod_exists.stat.exists) - (not compose_base_exists.stat.exists or not compose_override_exists.stat.exists)
- name: Create backup directory - name: Create backup directory
file: file:
@@ -94,10 +99,10 @@
register: compose_base_check register: compose_base_check
when: not (application_sync_files | default(false) | bool) when: not (application_sync_files | default(false) | bool)
- name: Verify docker-compose.production.yml exists - name: Verify docker-compose override file exists (production or staging)
stat: stat:
path: "{{ app_stack_path }}/docker-compose.production.yml" path: "{{ app_stack_path }}/docker-compose.{{ application_compose_suffix }}"
register: compose_prod_check register: compose_override_check
when: not (application_sync_files | default(false) | bool) when: not (application_sync_files | default(false) | bool)
- name: Fail if docker-compose files don't exist - name: Fail if docker-compose files don't exist
@@ -107,7 +112,7 @@
Required files: Required files:
- docker-compose.base.yml - docker-compose.base.yml
- docker-compose.production.yml - docker-compose.{{ application_compose_suffix }}
The Application Stack must be deployed first via: The Application Stack must be deployed first via:
ansible-playbook -i inventory/production.yml playbooks/setup-infrastructure.yml ansible-playbook -i inventory/production.yml playbooks/setup-infrastructure.yml
@@ -115,12 +120,12 @@
This will create the application stack with docker-compose files and .env file. This will create the application stack with docker-compose files and .env file.
when: when:
- not (application_sync_files | default(false) | bool) - not (application_sync_files | default(false) | bool)
- (not compose_base_check.stat.exists or not compose_prod_check.stat.exists) - (not compose_base_check.stat.exists or not compose_override_check.stat.exists)
- name: Backup current deployment metadata - name: Backup current deployment metadata
shell: | shell: |
docker compose -f {{ app_stack_path }}/docker-compose.base.yml -f {{ app_stack_path }}/docker-compose.production.yml ps --format json 2>/dev/null > {{ backups_path }}/{{ deployment_timestamp | regex_replace(':', '-') }}/current_containers.json || true docker compose -f {{ app_stack_path }}/docker-compose.base.yml -f {{ app_stack_path }}/docker-compose.{{ application_compose_suffix }} ps --format json 2>/dev/null > {{ backups_path }}/{{ deployment_timestamp | regex_replace(':', '-') }}/current_containers.json || true
docker compose -f {{ app_stack_path }}/docker-compose.base.yml -f {{ app_stack_path }}/docker-compose.production.yml config 2>/dev/null > {{ backups_path }}/{{ deployment_timestamp | regex_replace(':', '-') }}/docker-compose-config.yml || true docker compose -f {{ app_stack_path }}/docker-compose.base.yml -f {{ app_stack_path }}/docker-compose.{{ application_compose_suffix }} config 2>/dev/null > {{ backups_path }}/{{ deployment_timestamp | regex_replace(':', '-') }}/docker-compose-config.yml || true
args: args:
executable: /bin/bash executable: /bin/bash
changed_when: false changed_when: false
@@ -128,7 +133,7 @@
when: when:
- not (application_sync_files | default(false) | bool) - not (application_sync_files | default(false) | bool)
- compose_base_exists.stat.exists | default(false) - compose_base_exists.stat.exists | default(false)
- compose_prod_exists.stat.exists | default(false) - compose_override_exists.stat.exists | default(false)
- name: Login to Docker registry (if credentials provided) - name: Login to Docker registry (if credentials provided)
community.docker.docker_login: community.docker.docker_login:
@@ -167,9 +172,9 @@
application_remove_orphans: false application_remove_orphans: false
when: application_sync_files | default(false) | bool when: application_sync_files | default(false) | bool
- name: Update docker-compose.production.yml with new image tag (all services) - name: Update docker-compose override file with new image tag (all services)
replace: replace:
path: "{{ app_stack_path }}/docker-compose.production.yml" path: "{{ app_stack_path }}/docker-compose.{{ application_compose_suffix }}"
# Match both localhost:5000 and registry.michaelschiemer.de (or any registry URL) # Match both localhost:5000 and registry.michaelschiemer.de (or any registry URL)
regexp: '^(\s+image:\s+)(localhost:5000|registry\.michaelschiemer\.de|{{ docker_registry }})/{{ app_name }}:.*$' regexp: '^(\s+image:\s+)(localhost:5000|registry\.michaelschiemer\.de|{{ docker_registry }})/{{ app_name }}:.*$'
replace: '\1{{ app_image }}:{{ image_tag }}' replace: '\1{{ app_image }}:{{ image_tag }}'

View File

@@ -34,3 +34,13 @@ application_wait_interval: 5
# Command executed inside the app container to run migrations # Command executed inside the app container to run migrations
application_migration_command: "php console.php db:migrate" application_migration_command: "php console.php db:migrate"
# Environment (production, staging, local)
# Determines which compose files to use and service names
application_environment: "{{ APP_ENV | default('production') }}"
# Compose file suffix based on environment
application_compose_suffix: "{{ 'staging.yml' if application_environment == 'staging' else 'production.yml' }}"
# Service names based on environment
application_service_name: "{{ 'staging-app' if application_environment == 'staging' else 'php' }}"

View File

@@ -10,7 +10,7 @@
- name: Wait for application container to report Up - name: Wait for application container to report Up
shell: | shell: |
docker compose -f {{ application_stack_dest }}/docker-compose.base.yml -f {{ application_stack_dest }}/docker-compose.production.yml ps php | grep -Eiq "Up|running" docker compose -f {{ application_stack_dest }}/docker-compose.base.yml -f {{ application_stack_dest }}/docker-compose.{{ application_compose_suffix }} ps {{ application_service_name }} | grep -Eiq "Up|running"
register: application_app_running register: application_app_running
changed_when: false changed_when: false
until: application_app_running.rc == 0 until: application_app_running.rc == 0
@@ -20,7 +20,7 @@
- name: Ensure app container is running before migrations - name: Ensure app container is running before migrations
shell: | shell: |
docker compose -f {{ application_stack_dest }}/docker-compose.base.yml -f {{ application_stack_dest }}/docker-compose.production.yml ps php | grep -Eiq "Up|running" docker compose -f {{ application_stack_dest }}/docker-compose.base.yml -f {{ application_stack_dest }}/docker-compose.{{ application_compose_suffix }} ps {{ application_service_name }} | grep -Eiq "Up|running"
args: args:
executable: /bin/bash executable: /bin/bash
register: application_app_container_running register: application_app_container_running
@@ -30,7 +30,7 @@
- name: Run database migrations - name: Run database migrations
shell: | shell: |
docker compose -f {{ application_stack_dest }}/docker-compose.base.yml -f {{ application_stack_dest }}/docker-compose.production.yml exec -T php {{ application_migration_command }} docker compose -f {{ application_stack_dest }}/docker-compose.base.yml -f {{ application_stack_dest }}/docker-compose.{{ application_compose_suffix }} exec -T {{ application_service_name }} {{ application_migration_command }}
args: args:
executable: /bin/bash executable: /bin/bash
register: application_migration_result register: application_migration_result
@@ -43,7 +43,7 @@
- application_app_container_running.rc == 0 - application_app_container_running.rc == 0
- name: Collect application container status - name: Collect application container status
shell: docker compose -f {{ application_stack_dest }}/docker-compose.base.yml -f {{ application_stack_dest }}/docker-compose.production.yml ps shell: docker compose -f {{ application_stack_dest }}/docker-compose.base.yml -f {{ application_stack_dest }}/docker-compose.{{ application_compose_suffix }} ps
register: application_ps register: application_ps
changed_when: false changed_when: false
ignore_errors: yes ignore_errors: yes

View File

@@ -80,11 +80,11 @@
register: application_compose_base_src register: application_compose_base_src
become: no become: no
- name: Check if application docker-compose.production.yml source exists locally - name: Check if application docker-compose override file exists locally (production or staging)
stat: stat:
path: "{{ application_stack_src }}/../../../docker-compose.production.yml" path: "{{ application_stack_src }}/../../../docker-compose.{{ application_compose_suffix }}"
delegate_to: localhost delegate_to: localhost
register: application_compose_prod_src register: application_compose_override_src
become: no become: no
- name: Copy application docker-compose.base.yml to target host - name: Copy application docker-compose.base.yml to target host
@@ -96,14 +96,14 @@
mode: '0644' mode: '0644'
when: application_compose_base_src.stat.exists when: application_compose_base_src.stat.exists
- name: Copy application docker-compose.production.yml to target host - name: Copy application docker-compose override file to target host (production or staging)
copy: copy:
src: "{{ application_stack_src }}/../../../docker-compose.production.yml" src: "{{ application_stack_src }}/../../../docker-compose.{{ application_compose_suffix }}"
dest: "{{ application_stack_dest }}/docker-compose.production.yml" dest: "{{ application_stack_dest }}/docker-compose.{{ application_compose_suffix }}"
owner: "{{ ansible_user }}" owner: "{{ ansible_user }}"
group: "{{ ansible_user }}" group: "{{ ansible_user }}"
mode: '0644' mode: '0644'
when: application_compose_prod_src.stat.exists when: application_compose_override_src.stat.exists
- name: Check if legacy docker-compose.yml exists (fallback) - name: Check if legacy docker-compose.yml exists (fallback)
stat: stat:

View File

@@ -41,6 +41,22 @@ services:
# Only bind to localhost, not external interfaces # Only bind to localhost, not external interfaces
# Default port 3001 to avoid conflict with Gitea (port 3000) # Default port 3001 to avoid conflict with Gitea (port 3000)
- "127.0.0.1:${SEMAPHORE_PORT:-3001}:3000" - "127.0.0.1:${SEMAPHORE_PORT:-3001}:3000"
labels:
# Traefik configuration
- "traefik.enable=true"
# HTTP Router (redirects to HTTPS)
- "traefik.http.routers.semaphore.rule=Host(`semaphore.michaelschiemer.de`)"
- "traefik.http.routers.semaphore.entrypoints=web"
- "traefik.http.routers.semaphore.middlewares=redirect-to-https"
# HTTPS Router
- "traefik.http.routers.semaphore-secure.rule=Host(`semaphore.michaelschiemer.de`)"
- "traefik.http.routers.semaphore-secure.entrypoints=websecure"
- "traefik.http.routers.semaphore-secure.tls=true"
- "traefik.http.routers.semaphore-secure.tls.certresolver=letsencrypt"
- "traefik.http.routers.semaphore-secure.service=semaphore"
# Service definition (use container IP in host network mode)
- "traefik.http.services.semaphore.loadbalancer.server.scheme=http"
- "traefik.http.services.semaphore.loadbalancer.server.port=3000"
environment: environment:
- TZ=Europe/Berlin - TZ=Europe/Berlin
# Database Configuration # Database Configuration
@@ -68,7 +84,7 @@ services:
- /etc/timezone:/etc/timezone:ro - /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro - /etc/localtime:/etc/localtime:ro
healthcheck: healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/api/health"] test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/"]
interval: 30s interval: 30s
timeout: 10s timeout: 10s
retries: 3 retries: 3

View File

@@ -0,0 +1,18 @@
http:
routers:
semaphore:
rule: Host(`semaphore.michaelschiemer.de`)
entrypoints:
- websecure
service: semaphore
tls:
certResolver: letsencrypt
priority: 100
services:
semaphore:
loadBalancer:
# Use localhost port binding since Semaphore binds to 127.0.0.1
# Check actual port with: docker ps | grep semaphore
# Default is 3001, but may be 9300 if SEMAPHORE_PORT env var is set differently
servers:
- url: http://127.0.0.1:3001

View File

@@ -248,19 +248,38 @@ services:
- REDIS_PASSWORD_FILE=/run/secrets/redis_password - REDIS_PASSWORD_FILE=/run/secrets/redis_password
secrets: secrets:
- redis_password - redis_password
command: > # Use entrypoint script to inject password from Docker Secret into config
sh -c " # Note: Script runs as root to read Docker Secrets, then starts Redis
REDIS_PASSWORD=$$(cat /run/secrets/redis_password 2>/dev/null || echo ${REDIS_PASSWORD}) entrypoint: ["/bin/sh", "-c"]
redis-server command:
--requirepass $$REDIS_PASSWORD - |
--maxmemory 256mb # Read password from Docker Secret (as root)
--maxmemory-policy allkeys-lru REDIS_PASSWORD=$$(cat /run/secrets/redis_password 2>/dev/null || echo '')
--save 900 1 # Start Redis with all settings as command line arguments (no config file to avoid conflicts)
--save 300 10 if [ -n "$$REDIS_PASSWORD" ]; then
--save 60 10000 exec redis-server \
--appendonly yes --bind 0.0.0.0 \
--dir /data \
--maxmemory 256mb \
--maxmemory-policy allkeys-lru \
--save 900 1 \
--save 300 10 \
--save 60 10000 \
--appendonly yes \
--appendfsync everysec \
--requirepass "$$REDIS_PASSWORD"
else
exec redis-server \
--bind 0.0.0.0 \
--dir /data \
--maxmemory 256mb \
--maxmemory-policy allkeys-lru \
--save 900 1 \
--save 300 10 \
--save 60 10000 \
--appendonly yes \
--appendfsync everysec --appendfsync everysec
" fi
volumes: volumes:
- staging-redis-data:/data - staging-redis-data:/data
- /etc/timezone:/etc/timezone:ro - /etc/timezone:/etc/timezone:ro