feat(deployment): update semaphore configuration and deployment workflows
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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 }}'
|
||||||
|
|||||||
@@ -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' }}"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
18
deployment/stacks/traefik/dynamic/semaphore.yml
Normal file
18
deployment/stacks/traefik/dynamic/semaphore.yml
Normal 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
|
||||||
@@ -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 \
|
||||||
--appendfsync everysec
|
--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
|
||||||
|
fi
|
||||||
volumes:
|
volumes:
|
||||||
- staging-redis-data:/data
|
- staging-redis-data:/data
|
||||||
- /etc/timezone:/etc/timezone:ro
|
- /etc/timezone:/etc/timezone:ro
|
||||||
|
|||||||
Reference in New Issue
Block a user