From 0f9fbc9375c5676294d2701a7382f3166674a7b8 Mon Sep 17 00:00:00 2001 From: lubiana Date: Thu, 24 Jul 2025 11:35:37 +0200 Subject: [PATCH] wip --- Containerfile | 10 -- README.md | 22 +++- build-and-push.sh | 18 +++ build-container.sh | 40 ------- docker-compose.yml | 6 + frontend/src/components/ChecklistItem.tsx | 59 ++++++---- frontend/src/components/CreateChecklist.tsx | 76 ++++++++++++ frontend/src/components/SavedChecklists.tsx | 82 +++++++++++++ frontend/src/hooks/useLocalStorage.ts | 89 ++++++-------- frontend/src/hooks/useSSE.ts | 14 ++- frontend/src/pages/Checklist.tsx | 71 +++++------- frontend/src/pages/Home.tsx | 121 +------------------- frontend/src/types.ts | 4 +- main.go | 28 ++++- podman-compose.yml | 38 ++++++ 15 files changed, 381 insertions(+), 297 deletions(-) create mode 100755 build-and-push.sh delete mode 100755 build-container.sh create mode 100644 frontend/src/components/CreateChecklist.tsx create mode 100644 frontend/src/components/SavedChecklists.tsx create mode 100644 podman-compose.yml diff --git a/Containerfile b/Containerfile index 1370242..dcc0107 100644 --- a/Containerfile +++ b/Containerfile @@ -38,22 +38,12 @@ FROM alpine:latest # Install runtime dependencies for SQLite RUN apk add --no-cache ca-certificates sqlite -# Create non-root user -RUN addgroup -g 1001 -S gocheck && \ - adduser -S -D -H -h /app -s /sbin/nologin -G gocheck -g gocheck -u 1001 gocheck - # Copy the binary from builder stage COPY --from=builder /app/gocheck /app/gocheck # Set working directory WORKDIR /app -# Change ownership to non-root user -RUN chown -R gocheck:gocheck /app - -# Switch to non-root user -USER gocheck - # Expose default port (can be overridden via PORT env var) EXPOSE 8080 diff --git a/README.md b/README.md index 549ee11..b4dee67 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,8 @@ deployment: #### Using Podman (Recommended) +The application is optimized for rootless Podman operation: + ```bash # Build and run with the provided script ./build-container.sh @@ -125,8 +127,21 @@ podman run -d --name gocheck-container -p 8080:8080 -v gocheck-data:/app gocheck PORT=3000 ./build-container.sh # Or manually: podman run -d --name gocheck-container -p 3000:8080 -e PORT=8080 -v gocheck-data:/app gocheck + +# Using Podman Compose (recommended for rootless): +podman-compose -f podman-compose.yml up -d + +# Or with Docker Compose (also works with Podman): +docker-compose up -d ``` +**Rootless Podman Compatibility:** + +- Uses UID/GID 1000 for better user mapping +- Includes security options for rootless operation +- Supports both `podman-compose` and `docker-compose` +- Alternative compose file available: `podman-compose.yml` + #### Using Docker ```bash @@ -142,10 +157,11 @@ docker run -d --name gocheck-container -p 3000:8080 -e PORT=8080 -v gocheck-data #### Container Features -- **Multi-stage build**: Uses Go Alpine for building, distroless for runtime -- **Minimal footprint**: Runtime container is only ~2MB +- **Multi-stage build**: Uses Go Alpine for building, Alpine for runtime +- **Minimal footprint**: Runtime container is optimized for size - **Persistent data**: SQLite database is stored in a named volume -- **Security**: Runs as non-root user in distroless container +- **Security**: Runs as non-root user with UID 1000 +- **Rootless Podman compatible**: Optimized for rootless container operation - **Configurable**: Supports environment variables for customization #### Environment Variables diff --git a/build-and-push.sh b/build-and-push.sh new file mode 100755 index 0000000..5de58fc --- /dev/null +++ b/build-and-push.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env sh + +MANIFEST=git.hannover.ccc.de/lubiana/cheekylist +AMD=${MANIFEST}amd64 +ARM=${MANIFEST}arm64 + +podman manifest exists $MANIFEST +if [ $? -eq 0 ]; then + podman manifest rm $MANIFEST +fi +podman manifest create $MANIFEST + +podman build --arch=amd64 -t $AMD . +podman build --arch=arm64 -t $ARM . + +podman manifest add $MANIFEST $AMD $ARM + +podman manifest push $MANIFEST diff --git a/build-container.sh b/build-container.sh deleted file mode 100755 index 973fcb4..0000000 --- a/build-container.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/bin/bash - -# Build script for GoCheck container -set -e - -IMAGE_NAME="gocheck" -CONTAINER_NAME="gocheck-container" -PORT="${PORT:-8080}" - -echo "��️ Building GoCheck frontend..." - - -echo "🏗️ Building GoCheck container..." - -# Build the container image -podman build -t $IMAGE_NAME -f Containerfile . - -echo "✅ Container built successfully!" - -echo "🚀 Starting GoCheck container on port $PORT..." -podman run -d \ - --rm \ - --name $CONTAINER_NAME \ - -p $PORT:8080 \ - -e PORT=8080 \ - -v gocheck-data:/app \ - $IMAGE_NAME - -echo "✅ GoCheck is running!" -echo "🌐 Access the application at: http://localhost:$PORT" -echo "" -echo "📋 Container management commands:" -echo " View logs: podman logs $CONTAINER_NAME" -echo " Stop: podman stop $CONTAINER_NAME" -echo " Start: podman start $CONTAINER_NAME" -echo " Remove: podman rm $CONTAINER_NAME" -echo " Shell access: podman exec -it $CONTAINER_NAME /bin/sh" -echo "" -echo "🔧 Environment variables:" -echo " PORT: Set to override the default port (default: 8080)" \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 072d87f..628485e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,6 +5,9 @@ services: build: context: . dockerfile: Containerfile + args: + USER_ID: 1000 + GROUP_ID: 1000 container_name: gocheck-container ports: - "${PORT:-8080}:8080" @@ -13,6 +16,9 @@ services: volumes: - gocheck-data:/app restart: unless-stopped + # Rootless Podman compatibility + security_opt: + - no-new-privileges:true healthcheck: test: [ "CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/" ] interval: 30s diff --git a/frontend/src/components/ChecklistItem.tsx b/frontend/src/components/ChecklistItem.tsx index d17f290..5433e70 100644 --- a/frontend/src/components/ChecklistItem.tsx +++ b/frontend/src/components/ChecklistItem.tsx @@ -124,27 +124,33 @@ export default function ChecklistItem({ } return ( -
0 ? 'ml-4 border-l-2 border-gray-200 dark:border-gray-700 pl-3' : ''}`}> -
-
+
  • +
    + {/* Checkbox */} +
    - +
    + + {/* Content */} +
    -
    + {/* Actions */} +
    {isLocked && !isLockedByMe && ( - - 🔒 {item.locked_by} + + + + + {item.locked_by} )} {!isEditing && ( - <> - - + )}
    + {/* Children */} {children.length > 0 && ( -
    +
    {children.map(child => ( )} -
    +
  • ) } \ No newline at end of file diff --git a/frontend/src/components/CreateChecklist.tsx b/frontend/src/components/CreateChecklist.tsx new file mode 100644 index 0000000..9a46447 --- /dev/null +++ b/frontend/src/components/CreateChecklist.tsx @@ -0,0 +1,76 @@ +import { useState } from 'react' +import { useNavigate } from 'react-router-dom' + +interface CreateChecklistProps { + className?: string +} + +export default function CreateChecklist({ className = '' }: CreateChecklistProps) { + const [checklistName, setChecklistName] = useState('') + const [isCreating, setIsCreating] = useState(false) + const navigate = useNavigate() + + const createChecklist = async () => { + if (!checklistName.trim()) return + + setIsCreating(true) + try { + const response = await fetch('/api/checklists', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ name: checklistName.trim() }), + }) + + if (response.ok) { + const data = await response.json() + + // Navigate to the new checklist + navigate(`/${data.uuid}`) + } else { + alert('Failed to create checklist') + } + } catch (error) { + console.error('Error creating checklist:', error) + alert('Failed to create checklist') + } finally { + setIsCreating(false) + setChecklistName('') + } + } + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + createChecklist() + } + } + + return ( +
    +

    Create New Checklist

    +
    + setChecklistName(e.target.value)} + onKeyDown={handleKeyDown} + disabled={isCreating} + className="flex-1 px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:bg-gray-100 dark:disabled:bg-gray-700 disabled:text-gray-500 dark:disabled:text-gray-400 bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400" + /> + +
    +
    + ) +} \ No newline at end of file diff --git a/frontend/src/components/SavedChecklists.tsx b/frontend/src/components/SavedChecklists.tsx new file mode 100644 index 0000000..a482e9d --- /dev/null +++ b/frontend/src/components/SavedChecklists.tsx @@ -0,0 +1,82 @@ +import { Link } from 'react-router-dom' +import { loadSavedChecklists } from '../hooks/useLocalStorage' +import { useState } from 'react' + +interface SavedChecklistsProps { + className?: string +} + +export default function SavedChecklists({ className = '' }: SavedChecklistsProps) { + const [savedChecklists, setSavedChecklists] = useState<{ uuid: string; name: string; lastOpened?: string }[]>(loadSavedChecklists()) + const handleForgetCheckList = (uuid: string): void => { + const updatedChecklists = savedChecklists.filter((checklist: { uuid: string }) => checklist.uuid !== uuid) + localStorage.setItem('gocheck-saved-checklists', JSON.stringify(updatedChecklists)) + setSavedChecklists(updatedChecklists) + } + + + if (savedChecklists.length === 0) { + return ( +
    +
    📋
    +

    No checklists yet

    +

    Create your first checklist above to get started!

    +
    + ) + } + + return ( +
    +

    Your Checklists

    +
    + {savedChecklists.map((checklist) => ( +
    +
    +

    {checklist.name}

    +

    + Last opened: {checklist.lastOpened ? new Date(checklist.lastOpened).toLocaleString() : 'Never'} +

    +
    +
    + + + + + +
    +
    + ))} +
    +
    + ) +} \ No newline at end of file diff --git a/frontend/src/hooks/useLocalStorage.ts b/frontend/src/hooks/useLocalStorage.ts index ca3d59d..d6b9ce1 100644 --- a/frontend/src/hooks/useLocalStorage.ts +++ b/frontend/src/hooks/useLocalStorage.ts @@ -1,69 +1,44 @@ -import { useState, useEffect } from 'react' import type { SavedChecklist } from '../types' const STORAGE_KEY = 'gocheck-saved-checklists' -export function useLocalStorage() { - const [savedChecklists, setSavedChecklists] = useState([]) - - useEffect(() => { - loadSavedChecklists() - }, []) - - const loadSavedChecklists = () => { +export function saveChecklist(checklist: SavedChecklist) { try { - const saved = localStorage.getItem(STORAGE_KEY) - if (saved) { - setSavedChecklists(JSON.parse(saved)) + const existingChecklists = loadSavedChecklists() + const existingIndex = existingChecklists.findIndex(c => c.uuid === checklist.uuid); + + if (existingIndex > -1) { + // Update lastOpened timestamp if checklist already exists + existingChecklists[existingIndex].lastOpened = new Date().toISOString(); + } else { + // Add new checklist with lastOpened timestamp + existingChecklists.push({ ...checklist, lastOpened: new Date().toISOString() }); } + + // Sort by lastOpened date in descending order + existingChecklists.sort((a, b) => { + if (!a.lastOpened) return 1; + if (!b.lastOpened) return -1; + return new Date(b.lastOpened).getTime() - new Date(a.lastOpened).getTime(); + }); + + localStorage.setItem(STORAGE_KEY, JSON.stringify(existingChecklists)); } catch (error) { - console.error('Error loading saved checklists:', error) + console.error('Error saving checklist:', error); } - } +} - const saveChecklist = (checklist: SavedChecklist) => { - try { - const updated = [checklist, ...savedChecklists.filter(c => c.uuid !== checklist.uuid)] - localStorage.setItem(STORAGE_KEY, JSON.stringify(updated)) - setSavedChecklists(updated) - } catch (error) { - console.error('Error saving checklist:', error) +export const loadSavedChecklists = (): SavedChecklist[] => { + const saved = localStorage.getItem(STORAGE_KEY) + if (saved) { + try { + const parsed = JSON.parse(saved); + if (Array.isArray(parsed)) { + return parsed as SavedChecklist[]; } + } catch (error) { + console.error('Error parsing saved checklists:', error); } - - const deleteChecklist = (uuid: string) => { - try { - const updated = savedChecklists.filter(c => c.uuid !== uuid) - localStorage.setItem(STORAGE_KEY, JSON.stringify(updated)) - setSavedChecklists(updated) - } catch (error) { - console.error('Error deleting checklist:', error) - } } - - const updateChecklistName = (uuid: string, newName: string) => { - try { - const updated = savedChecklists.map(c => - c.uuid === uuid ? { ...c, name: newName } : c - ) - localStorage.setItem(STORAGE_KEY, JSON.stringify(updated)) - setSavedChecklists(updated) - } catch (error) { - console.error('Error updating checklist name:', error) - } - } - - const getChecklistName = (uuid: string): string | null => { - const checklist = savedChecklists.find(c => c.uuid === uuid) - return checklist ? checklist.name : null - } - - return { - savedChecklists, - saveChecklist, - deleteChecklist, - updateChecklistName, - getChecklistName, - loadSavedChecklists - } -} \ No newline at end of file + return [] +} diff --git a/frontend/src/hooks/useSSE.ts b/frontend/src/hooks/useSSE.ts index 203bc73..ff98ce7 100644 --- a/frontend/src/hooks/useSSE.ts +++ b/frontend/src/hooks/useSSE.ts @@ -1,8 +1,10 @@ import { useEffect, useRef, useState } from 'react' import type { SSEEvent, ChecklistItem } from '../types' +import { saveChecklist } from '../hooks/useLocalStorage' export function useSSE(uuid: string) { const [items, setItems] = useState([]) + const [checkListName, setCheckListName] = useState('') const [isConnected, setIsConnected] = useState(false) const [error, setError] = useState(null) const eventSourceRef = useRef(null) @@ -25,6 +27,16 @@ export function useSSE(uuid: string) { const data: SSEEvent = JSON.parse(event.data) switch (data.type) { + case 'checklist_name': + if (data.name) { + saveChecklist({ + uuid, + name: data.name, + createdAt: new Date().toISOString(), + }) + setCheckListName(data.name) + } + break case 'full_state': if (data.items) { setItems(data.items) @@ -109,5 +121,5 @@ export function useSSE(uuid: string) { } }, [uuid]) - return { items, isConnected, error } + return { items, checkListName, isConnected, error } } \ No newline at end of file diff --git a/frontend/src/pages/Checklist.tsx b/frontend/src/pages/Checklist.tsx index 40f6fe0..147dac5 100644 --- a/frontend/src/pages/Checklist.tsx +++ b/frontend/src/pages/Checklist.tsx @@ -1,30 +1,15 @@ -import { useState, useEffect } from 'react' -import { useParams, useNavigate } from 'react-router-dom' +import { useState } from 'react' +import { useParams, Link } from 'react-router-dom' import { useSSE } from '../hooks/useSSE' -import { useLocalStorage } from '../hooks/useLocalStorage' import ChecklistItem from '../components/ChecklistItem' import type { ChecklistItem as ChecklistItemType } from '../types' export default function Checklist() { const { uuid } = useParams<{ uuid: string }>() - const navigate = useNavigate() - const { items, isConnected, error } = useSSE(uuid || '') - const { saveChecklist, getChecklistName } = useLocalStorage() + const { items, checkListName, isConnected, error } = useSSE(uuid || '') const [newItemContent, setNewItemContent] = useState('') const [isAddingItem, setIsAddingItem] = useState(false) - // Save checklist to local storage if not already saved - useEffect(() => { - if (uuid && !getChecklistName(uuid)) { - // Generate a default name if not in local storage - const defaultName = `Checklist ${new Date().toLocaleDateString()}` - saveChecklist({ - uuid, - name: defaultName, - createdAt: new Date().toISOString() - }) - } - }, [uuid, saveChecklist, getChecklistName]) const buildItemTree = (items: ChecklistItemType[]): ChecklistItemType[] => { const itemMap = new Map() @@ -169,33 +154,33 @@ export default function Checklist() { ) } - const checklistName = getChecklistName(uuid) || 'Untitled Checklist' const itemTree = buildItemTree(items) return (
    {/* Header */}
    -
    -
    -
    +
    +
    +
    + -

    {checklistName}

    + +

    {checkListName}

    {isConnected ? ( - + Connected ) : ( - + Disconnected @@ -207,8 +192,8 @@ export default function Checklist() { {/* Error Message */} {error && ( -
    -
    +
    +
    ⚠️ {error} @@ -218,23 +203,23 @@ export default function Checklist() { )} {/* Main Content */} -
    +
    {/* Add Item Section */}
    -
    +
    setNewItemContent(e.target.value)} - onKeyPress={handleKeyPress} + onKeyDown={handleKeyPress} disabled={isAddingItem || !isConnected} - className="flex-1 px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:bg-gray-100 dark:disabled:bg-gray-700 disabled:text-gray-500 dark:disabled:text-gray-400 bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400" + className="flex-1 px-3 py-1 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:bg-gray-100 dark:disabled:bg-gray-700 disabled:text-gray-500 dark:disabled:text-gray-400 bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400" /> @@ -242,17 +227,17 @@ export default function Checklist() {
    {/* Items Section */} -
    +
    {itemTree.length === 0 ? ( -
    -
    📝
    -

    No items yet

    -

    Add your first item above to get started!

    +
    +
    📝
    +

    No items yet

    +

    Add your first item above to get started!

    ) : ( -
    +
      {renderItems(itemTree)} -
    + )}
    diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index 17419d4..8873ad1 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -1,129 +1,16 @@ -import { useState } from 'react' -import { useNavigate } from 'react-router-dom' -import { useLocalStorage } from '../hooks/useLocalStorage' -import type { SavedChecklist } from '../types' +import CreateChecklist from '../components/CreateChecklist' +import SavedChecklists from '../components/SavedChecklists' function Home() { - const [checklistName, setChecklistName] = useState('') - const [isCreating, setIsCreating] = useState(false) - const navigate = useNavigate() - const { savedChecklists, saveChecklist, deleteChecklist } = useLocalStorage() - - const createChecklist = async () => { - if (!checklistName.trim()) return - - setIsCreating(true) - try { - const response = await fetch('/api/checklists', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ name: checklistName.trim() }), - }) - - if (response.ok) { - const data = await response.json() - const newChecklist: SavedChecklist = { - uuid: data.uuid, - name: checklistName.trim(), - createdAt: new Date().toISOString(), - } - - // Save to local storage - saveChecklist(newChecklist) - - // Navigate to the new checklist - navigate(`/${data.uuid}`) - } else { - alert('Failed to create checklist') - } - } catch (error) { - console.error('Error creating checklist:', error) - alert('Failed to create checklist') - } finally { - setIsCreating(false) - setChecklistName('') - } - } - - const handleDeleteChecklist = (uuid: string) => { - deleteChecklist(uuid) - } - - // Note: handleUpdateChecklistName is available for future use - // const handleUpdateChecklistName = (uuid: string, newName: string) => { - // updateChecklistName(uuid, newName) - // } - return (
    {/* Create New Checklist Section */} -
    -

    Create New Checklist

    -
    - setChecklistName(e.target.value)} - onKeyPress={(e) => e.key === 'Enter' && createChecklist()} - disabled={isCreating} - className="flex-1 px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:bg-gray-100 dark:disabled:bg-gray-700 disabled:text-gray-500 dark:disabled:text-gray-400 bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400" - /> - -
    -
    + {/* Saved Checklists Section */} - {savedChecklists.length > 0 && ( -
    -

    Your Checklists

    -
    - {savedChecklists.map((checklist) => ( -
    -
    -

    {checklist.name}

    -

    - Created: {new Date(checklist.createdAt).toLocaleDateString()} -

    -
    -
    - - -
    -
    - ))} -
    -
    - )} - - {/* Empty State */} - {savedChecklists.length === 0 && ( -
    -
    📋
    -

    No checklists yet

    -

    Create your first checklist above to get started!

    -
    - )} +
    ) diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 81a491a..e2e60e5 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -13,10 +13,12 @@ export interface SavedChecklist { uuid: string name: string createdAt: string + lastOpened?: string } export interface SSEEvent { - type: 'full_state' | 'item_added' | 'item_updated' | 'item_deleted' | 'item_locked' | 'item_unlocked' + type: 'full_state' | 'item_added' | 'item_updated' | 'item_deleted' | 'item_locked' | 'item_unlocked' | 'checklist_name' + name?: string items?: ChecklistItem[] item?: ChecklistItem id?: number diff --git a/main.go b/main.go index 0eeb434..5819001 100644 --- a/main.go +++ b/main.go @@ -84,6 +84,25 @@ func setupDatabase() error { return nil } +func loadChecklistName(uuid string) (string, error) { + rows, err := db.Query( + `SELECT name FROM checklists WHERE uuid = ?`, + uuid) + if err != nil { + return "", err + } + defer rows.Close() + if rows.Next() { + var name string + err = rows.Scan(&name) + if err != nil { + return "", err + } + return name, nil + } + return "", fmt.Errorf("not found") +} + func loadChecklistItems(uuid string) ([]ChecklistItem, error) { rows, err := db.Query( `SELECT id, content, checked, parent_id FROM items WHERE checklist_uuid = ?`, @@ -427,13 +446,20 @@ func handleSSE(w http.ResponseWriter, r *http.Request) { // Send full state on connect items, err := loadChecklistItems(uuid) - if err == nil { + name, err2 := loadChecklistName(uuid) + if err == nil && err2 == nil { msg, _ := json.Marshal(map[string]interface{}{ "type": "full_state", "items": items, }) fmt.Fprintf(w, "data: %s\n\n", msg) flusher.Flush() + msg, _ = json.Marshal(map[string]interface{}{ + "type": "checklist_name", + "name": name, + }) + fmt.Fprintf(w, "data: %s\n\n", msg) + flusher.Flush() } // Forward events diff --git a/podman-compose.yml b/podman-compose.yml new file mode 100644 index 0000000..587251e --- /dev/null +++ b/podman-compose.yml @@ -0,0 +1,38 @@ +services: + gocheck: + build: + context: . + dockerfile: Containerfile + args: + USER_ID: 1000 + GROUP_ID: 1000 + container_name: gocheck-container + ports: + - "${PORT:-8080}:8080" + environment: + - PORT=8080 + volumes: + - gocheck-data:/app + restart: unless-stopped + # Rootless Podman compatibility + security_opt: + - no-new-privileges:true + # Use host networking for better rootless compatibility + network_mode: host + healthcheck: + test: [ + "CMD", + "wget", + "--no-verbose", + "--tries=1", + "--spider", + "http://localhost:8080/", + ] + interval: 30s + timeout: 10s + retries: 3 + start_period: 10s + +volumes: + gocheck-data: + driver: local