docker updates.
Publish Docker Image / build-and-push (push) Failing after 3m36s

This commit is contained in:
2026-07-14 13:56:54 +01:00
parent ff23474a54
commit c6ec8f6ad9
6 changed files with 156 additions and 61 deletions
+46
View File
@@ -0,0 +1,46 @@
name: Publish Docker Image
on:
push:
branches:
- main
tags:
- 'v*'
workflow_dispatch:
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to container registry
uses: docker/login-action@v3
with:
registry: git.lzstealth.com
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Extract image metadata
id: meta
uses: docker/metadata-action@v5
with:
images: git.lzstealth.com/LZStealth/pulse-signage
tags: |
type=raw,value=latest,enable={{is_default_branch}}
type=ref,event=tag
type=sha
- name: Build and push image
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
+13 -1
View File
@@ -68,9 +68,19 @@ The app reads its settings from environment variables.
The recommended way to run the project is with Docker Compose.
```bash
docker-compose up --build
npm run docker:up
```
The main Compose file now uses the published Docker image and still reads values from the repository `.env` file. `DB_HOST` comes from that file for the app containers, so you can point the web UI and player at an external database host or change it to `mysql` if you want to use the bundled MySQL service.
If you only want to test the image build from the current checkout without using Compose, run:
```bash
npm run docker:build
```
To publish the Docker image to the Gitea container registry, push to `main` or create a `v*` tag. The workflow in [.gitea/workflows/docker-publish.yml](.gitea/workflows/docker-publish.yml) builds `git.lzstealth.com/LZStealth/pulse-signage` and pushes `latest`, `sha`, and tagged releases.
This starts:
- the web admin app on port `3000`
@@ -79,6 +89,8 @@ This starts:
The web app and player app share the same uploaded media volume, so files uploaded in the admin UI are available to the player.
If you want to stop the stack later, run `npm run docker:down`. To follow logs, run `npm run docker:logs`.
## Important Notes
- The web app must be able to reach the player through `PLAYER_INTERNAL_BASE_URL`.
+14 -9
View File
@@ -1,12 +1,10 @@
version: "3.9"
services:
webui:
build:
context: .
dockerfile: Dockerfile
image: ${PULSE_SIGNAGE_IMAGE:-git.lzstealth.com/LZStealth/pulse-signage:latest}
container_name: signage-webui
restart: unless-stopped
env_file:
- .env
ports:
- "3000:3000"
environment:
@@ -20,6 +18,12 @@ services:
PLAYER_INTERNAL_BASE_URL: ${PLAYER_INTERNAL_BASE_URL:-http://player:3001}
PLAYER_PUBLIC_BASE_URL: ${PLAYER_PUBLIC_BASE_URL:-http://localhost:3001}
UPLOAD_DIR: ${UPLOAD_DIR:-/app/uploads}
SESSION_MAX_AGE_DAYS: ${SESSION_MAX_AGE_DAYS:-14}
DASHBOARD_REFRESH_INTERVAL_MS: ${DASHBOARD_REFRESH_INTERVAL_MS:-2000}
DEFAULT_ADMIN_USERNAME: ${DEFAULT_ADMIN_USERNAME:-admin}
DEFAULT_ADMIN_NAME: ${DEFAULT_ADMIN_NAME:-Admin}
DEFAULT_ADMIN_PASSWORD: ${DEFAULT_ADMIN_PASSWORD:-admin}
PASSWORD_HASH_ITERATIONS: ${PASSWORD_HASH_ITERATIONS:-310000}
volumes:
- uploads:/app/uploads
command: ["npm", "run", "start:webui"]
@@ -30,11 +34,11 @@ services:
- signage-net
player:
build:
context: .
dockerfile: Dockerfile
image: ${PULSE_SIGNAGE_IMAGE:-git.lzstealth.com/LZStealth/pulse-signage:latest}
container_name: signage-player
restart: unless-stopped
env_file:
- .env
ports:
- "3001:3001"
environment:
@@ -59,6 +63,8 @@ services:
image: mysql:8.4
container_name: signage-mysql
restart: unless-stopped
env_file:
- .env
ports:
- "3306:3306"
environment:
@@ -67,7 +73,6 @@ services:
MYSQL_PASSWORD: ${DB_PASSWORD:-signage_password}
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-root_password}
command:
- --default-authentication-plugin=mysql_native_password
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci
volumes:
+5 -1
View File
@@ -9,7 +9,11 @@
"start:webui": "node -r dotenv/config src/webui.js",
"start:player": "node -r dotenv/config src/player.js",
"dev:webui": "nodemon -r dotenv/config src/webui.js",
"dev:player": "nodemon -r dotenv/config src/player.js"
"dev:player": "nodemon -r dotenv/config src/player.js",
"docker:build": "docker build -t pulse-signage:test .",
"docker:up": "docker-compose up -d",
"docker:down": "docker-compose down",
"docker:logs": "docker-compose logs -f --tail=100"
},
"dependencies": {
"dotenv": "^17.4.2",
+64 -47
View File
@@ -14,6 +14,23 @@ function createPool() {
});
}
async function addColumnIfMissing(pool, tableName, columnName, columnDefinition) {
const [rows] = await pool.query(
`SELECT COUNT(*) AS column_count
FROM information_schema.columns
WHERE table_schema = DATABASE()
AND table_name = ?
AND column_name = ?`,
[tableName, columnName]
);
if (rows.length && Number(rows[0].column_count) > 0) {
return;
}
await pool.query(`ALTER TABLE \`${tableName}\` ADD COLUMN \`${columnName}\` ${columnDefinition}`);
}
async function ensureSchema(pool) {
await pool.query(`
CREATE TABLE IF NOT EXISTS canvas_sizes (
@@ -26,9 +43,9 @@ async function ensureSchema(pool) {
UNIQUE KEY uq_canvas_sizes_dimensions (width, height)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
`);
await pool.query('ALTER TABLE canvas_sizes ADD COLUMN IF NOT EXISTS modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP AFTER created_at');
await pool.query('ALTER TABLE canvas_sizes ADD COLUMN IF NOT EXISTS created_by INT NULL AFTER created_at');
await pool.query('ALTER TABLE canvas_sizes ADD COLUMN IF NOT EXISTS modified_by INT NULL AFTER modified_at');
await addColumnIfMissing(pool, 'canvas_sizes', 'modified_at', 'TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP');
await addColumnIfMissing(pool, 'canvas_sizes', 'created_by', 'INT NULL');
await addColumnIfMissing(pool, 'canvas_sizes', 'modified_by', 'INT NULL');
await pool.query(`
CREATE TABLE IF NOT EXISTS playlists (
@@ -39,10 +56,10 @@ async function ensureSchema(pool) {
modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
`);
await pool.query('ALTER TABLE playlists ADD COLUMN IF NOT EXISTS fade_between_slides TINYINT(1) NOT NULL DEFAULT 0 AFTER name');
await pool.query('ALTER TABLE playlists ADD COLUMN IF NOT EXISTS modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP AFTER created_at');
await pool.query('ALTER TABLE playlists ADD COLUMN IF NOT EXISTS created_by INT NULL AFTER created_at');
await pool.query('ALTER TABLE playlists ADD COLUMN IF NOT EXISTS modified_by INT NULL AFTER modified_at');
await addColumnIfMissing(pool, 'playlists', 'fade_between_slides', 'TINYINT(1) NOT NULL DEFAULT 0');
await addColumnIfMissing(pool, 'playlists', 'modified_at', 'TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP');
await addColumnIfMissing(pool, 'playlists', 'created_by', 'INT NULL');
await addColumnIfMissing(pool, 'playlists', 'modified_by', 'INT NULL');
await pool.query(`
CREATE TABLE IF NOT EXISTS slide_templates (
@@ -55,11 +72,11 @@ async function ensureSchema(pool) {
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
`);
await pool.query('ALTER TABLE slide_templates ADD COLUMN IF NOT EXISTS canvas_size_id INT NULL AFTER name');
await pool.query('ALTER TABLE slide_templates ADD COLUMN IF NOT EXISTS background_image_path VARCHAR(512) NULL');
await pool.query('ALTER TABLE slide_templates ADD COLUMN IF NOT EXISTS modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP AFTER created_at');
await pool.query('ALTER TABLE slide_templates ADD COLUMN IF NOT EXISTS created_by INT NULL AFTER created_at');
await pool.query('ALTER TABLE slide_templates ADD COLUMN IF NOT EXISTS modified_by INT NULL AFTER modified_at');
await addColumnIfMissing(pool, 'slide_templates', 'canvas_size_id', 'INT NULL');
await addColumnIfMissing(pool, 'slide_templates', 'background_image_path', 'VARCHAR(512) NULL');
await addColumnIfMissing(pool, 'slide_templates', 'modified_at', 'TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP');
await addColumnIfMissing(pool, 'slide_templates', 'created_by', 'INT NULL');
await addColumnIfMissing(pool, 'slide_templates', 'modified_by', 'INT NULL');
await pool.query(`
INSERT IGNORE INTO canvas_sizes (name, width, height) VALUES
@@ -103,10 +120,10 @@ async function ensureSchema(pool) {
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
`);
await pool.query('ALTER TABLE slide_template_regions ADD COLUMN IF NOT EXISTS font_family VARCHAR(100) NULL AFTER label');
await pool.query('ALTER TABLE slide_template_regions ADD COLUMN IF NOT EXISTS modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP AFTER created_at');
await pool.query('ALTER TABLE slide_template_regions ADD COLUMN IF NOT EXISTS created_by INT NULL AFTER created_at');
await pool.query('ALTER TABLE slide_template_regions ADD COLUMN IF NOT EXISTS modified_by INT NULL AFTER modified_at');
await addColumnIfMissing(pool, 'slide_template_regions', 'font_family', 'VARCHAR(100) NULL');
await addColumnIfMissing(pool, 'slide_template_regions', 'modified_at', 'TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP');
await addColumnIfMissing(pool, 'slide_template_regions', 'created_by', 'INT NULL');
await addColumnIfMissing(pool, 'slide_template_regions', 'modified_by', 'INT NULL');
await pool.query(`
CREATE TABLE IF NOT EXISTS slides (
@@ -122,14 +139,14 @@ async function ensureSchema(pool) {
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
`);
await pool.query('ALTER TABLE slides ADD COLUMN IF NOT EXISTS body TEXT NULL AFTER title');
await pool.query('ALTER TABLE slides ADD COLUMN IF NOT EXISTS template_id INT NULL AFTER body');
await pool.query('ALTER TABLE slides ADD COLUMN IF NOT EXISTS content_json JSON NULL AFTER template_id');
await pool.query('ALTER TABLE slides ADD COLUMN IF NOT EXISTS media_path VARCHAR(512) NULL AFTER content_json');
await pool.query('ALTER TABLE slides ADD COLUMN IF NOT EXISTS media_type VARCHAR(100) NULL AFTER media_path');
await pool.query('ALTER TABLE slides ADD COLUMN IF NOT EXISTS modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP AFTER created_at');
await pool.query('ALTER TABLE slides ADD COLUMN IF NOT EXISTS created_by INT NULL AFTER created_at');
await pool.query('ALTER TABLE slides ADD COLUMN IF NOT EXISTS modified_by INT NULL AFTER modified_at');
await addColumnIfMissing(pool, 'slides', 'body', 'TEXT NULL');
await addColumnIfMissing(pool, 'slides', 'template_id', 'INT NULL');
await addColumnIfMissing(pool, 'slides', 'content_json', 'JSON NULL');
await addColumnIfMissing(pool, 'slides', 'media_path', 'VARCHAR(512) NULL');
await addColumnIfMissing(pool, 'slides', 'media_type', 'VARCHAR(100) NULL');
await addColumnIfMissing(pool, 'slides', 'modified_at', 'TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP');
await addColumnIfMissing(pool, 'slides', 'created_by', 'INT NULL');
await addColumnIfMissing(pool, 'slides', 'modified_by', 'INT NULL');
await pool.query(`
CREATE TABLE IF NOT EXISTS playlist_slides (
@@ -151,16 +168,16 @@ async function ensureSchema(pool) {
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
`);
await pool.query('ALTER TABLE playlist_slides ADD COLUMN IF NOT EXISTS duration_seconds INT NOT NULL DEFAULT 10 AFTER position');
await pool.query("ALTER TABLE playlist_slides ADD COLUMN IF NOT EXISTS schedule_mode VARCHAR(20) NOT NULL DEFAULT 'always' AFTER duration_seconds");
await pool.query('ALTER TABLE playlist_slides ADD COLUMN IF NOT EXISTS schedule_start_datetime DATETIME NULL AFTER schedule_mode');
await pool.query('ALTER TABLE playlist_slides ADD COLUMN IF NOT EXISTS schedule_end_datetime DATETIME NULL AFTER schedule_start_datetime');
await pool.query('ALTER TABLE playlist_slides ADD COLUMN IF NOT EXISTS schedule_start_time TIME NULL AFTER schedule_end_datetime');
await pool.query('ALTER TABLE playlist_slides ADD COLUMN IF NOT EXISTS schedule_end_time TIME NULL AFTER schedule_start_time');
await pool.query('ALTER TABLE playlist_slides ADD COLUMN IF NOT EXISTS schedule_days_json JSON NULL AFTER schedule_end_time');
await pool.query('ALTER TABLE playlist_slides ADD COLUMN IF NOT EXISTS modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP AFTER created_at');
await pool.query('ALTER TABLE playlist_slides ADD COLUMN IF NOT EXISTS created_by INT NULL AFTER created_at');
await pool.query('ALTER TABLE playlist_slides ADD COLUMN IF NOT EXISTS modified_by INT NULL AFTER modified_at');
await addColumnIfMissing(pool, 'playlist_slides', 'duration_seconds', 'INT NOT NULL DEFAULT 10');
await addColumnIfMissing(pool, 'playlist_slides', 'schedule_mode', "VARCHAR(20) NOT NULL DEFAULT 'always'");
await addColumnIfMissing(pool, 'playlist_slides', 'schedule_start_datetime', 'DATETIME NULL');
await addColumnIfMissing(pool, 'playlist_slides', 'schedule_end_datetime', 'DATETIME NULL');
await addColumnIfMissing(pool, 'playlist_slides', 'schedule_start_time', 'TIME NULL');
await addColumnIfMissing(pool, 'playlist_slides', 'schedule_end_time', 'TIME NULL');
await addColumnIfMissing(pool, 'playlist_slides', 'schedule_days_json', 'JSON NULL');
await addColumnIfMissing(pool, 'playlist_slides', 'modified_at', 'TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP');
await addColumnIfMissing(pool, 'playlist_slides', 'created_by', 'INT NULL');
await addColumnIfMissing(pool, 'playlist_slides', 'modified_by', 'INT NULL');
await pool.query(`
CREATE TABLE IF NOT EXISTS screens (
@@ -174,10 +191,10 @@ async function ensureSchema(pool) {
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
`);
await pool.query('ALTER TABLE screens ADD COLUMN IF NOT EXISTS playlist_id INT NULL AFTER slug');
await pool.query('ALTER TABLE screens ADD COLUMN IF NOT EXISTS modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP AFTER created_at');
await pool.query('ALTER TABLE screens ADD COLUMN IF NOT EXISTS created_by INT NULL AFTER created_at');
await pool.query('ALTER TABLE screens ADD COLUMN IF NOT EXISTS modified_by INT NULL AFTER modified_at');
await addColumnIfMissing(pool, 'screens', 'playlist_id', 'INT NULL');
await addColumnIfMissing(pool, 'screens', 'modified_at', 'TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP');
await addColumnIfMissing(pool, 'screens', 'created_by', 'INT NULL');
await addColumnIfMissing(pool, 'screens', 'modified_by', 'INT NULL');
await pool.query(`
CREATE TABLE IF NOT EXISTS users (
@@ -191,13 +208,13 @@ async function ensureSchema(pool) {
modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
`);
await pool.query('ALTER TABLE users ADD COLUMN IF NOT EXISTS name VARCHAR(255) NULL AFTER id');
await pool.query('ALTER TABLE users ADD COLUMN IF NOT EXISTS password_hash CHAR(64) NOT NULL AFTER username');
await pool.query('ALTER TABLE users ADD COLUMN IF NOT EXISTS password_salt VARCHAR(64) NOT NULL AFTER password_hash');
await pool.query('ALTER TABLE users ADD COLUMN IF NOT EXISTS password_iterations INT NOT NULL AFTER password_salt');
await pool.query('ALTER TABLE users ADD COLUMN IF NOT EXISTS modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP AFTER created_at');
await pool.query('ALTER TABLE users ADD COLUMN IF NOT EXISTS created_by INT NULL AFTER created_at');
await pool.query('ALTER TABLE users ADD COLUMN IF NOT EXISTS modified_by INT NULL AFTER modified_at');
await addColumnIfMissing(pool, 'users', 'name', 'VARCHAR(255) NULL');
await addColumnIfMissing(pool, 'users', 'password_hash', 'CHAR(64) NOT NULL');
await addColumnIfMissing(pool, 'users', 'password_salt', 'VARCHAR(64) NOT NULL');
await addColumnIfMissing(pool, 'users', 'password_iterations', 'INT NOT NULL');
await addColumnIfMissing(pool, 'users', 'modified_at', 'TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP');
await addColumnIfMissing(pool, 'users', 'created_by', 'INT NULL');
await addColumnIfMissing(pool, 'users', 'modified_by', 'INT NULL');
await pool.query(`
CREATE TABLE IF NOT EXISTS auth_sessions (
@@ -209,8 +226,8 @@ async function ensureSchema(pool) {
CONSTRAINT fk_auth_sessions_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
`);
await pool.query('ALTER TABLE auth_sessions ADD COLUMN IF NOT EXISTS created_by INT NULL AFTER created_at');
await pool.query('ALTER TABLE auth_sessions ADD COLUMN IF NOT EXISTS modified_by INT NULL AFTER last_used_at');
await addColumnIfMissing(pool, 'auth_sessions', 'created_by', 'INT NULL');
await addColumnIfMissing(pool, 'auth_sessions', 'modified_by', 'INT NULL');
const [userCountRows] = await pool.query('SELECT COUNT(*) AS user_count FROM users');
if (!userCountRows.length || Number(userCountRows[0].user_count) === 0) {
+14 -3
View File
@@ -1,6 +1,17 @@
<script>
const slug = {{SLUG_JSON}};
const initialData = {{INITIAL_DATA_JSON}};
function parseTemplateJson(templateValue, fallbackValue) {
if (typeof templateValue !== 'string' || /^\s*\{\{.*\}\}\s*$/.test(templateValue)) {
return fallbackValue;
}
try {
return JSON.parse(templateValue);
} catch (_error) {
return fallbackValue;
}
}
const slug = parseTemplateJson('{{SLUG_JSON}}', '');
const initialData = parseTemplateJson('{{INITIAL_DATA_JSON}}', {});
const app = document.getElementById('app');
let slides = Array.isArray(initialData && initialData.slides) ? initialData.slides.map(normalizeSlide) : [];
let currentPlaylistSignature = '';
@@ -31,7 +42,7 @@
let slideExpiresAt = null;
const slideFadeDurationMs = 560;
const commandSocketPath = '/ws/screens/' + encodeURIComponent(slug);
const commandClientStorageKey = 'digital-signage-player-client-id:' + slug;
const commandClientStorageKey = 'pulse-signage-player-client-id:' + slug;
// Escape text before inserting it into HTML.
function escapeHtml(value) {