Add Docker support and improve PDF conversion

Features:
- Dockerfile for containerized deployment
- Docker Compose configuration with health checks
- Automated database initialization via entrypoint script
- Quick start script for easy Docker deployment
- Comprehensive Docker deployment documentation (DOCKER.md)
- Complete README with installation and usage instructions
- .dockerignore for optimized image builds

Improvements:
- PDF conversion now preserves orientation (portrait/landscape)
- PDF rendering at 300 DPI for sharp quality
- Maintains aspect ratio during conversion
- Compact media library view with image thumbnails
- Better media preview with scrollable gallery

Docker Features:
- Multi-stage build for smaller images
- Non-root user for security
- Health checks for container monitoring
- Volume mounts for persistent data
- Production-ready Gunicorn configuration
- Support for Redis caching (optional)
This commit is contained in:
DigiServer Developer
2025-11-17 21:05:49 +02:00
parent 2e3e181bb2
commit 2db0033bc0
9 changed files with 827 additions and 42 deletions

44
.dockerignore Normal file
View File

@@ -0,0 +1,44 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
venv/
env/
ENV/
.venv
# Development
.git/
.gitignore
*.md
*.sh
.vscode/
.idea/
# Database (will be created in volume)
instance/*.db
# Uploads (will be in volume)
app/static/uploads/*
static/uploads/*
# Logs
*.log
# OS
.DS_Store
Thumbs.db
# Documentation
BLUEPRINT_GUIDE.md
ICON_INTEGRATION.md
KIVY_PLAYER_COMPATIBILITY.md
PLAYER_AUTH.md
PROGRESS.md
README.md
# Config templates
player_config_template.ini
player_auth_module.py

252
DOCKER.md Normal file
View File

@@ -0,0 +1,252 @@
# Docker Deployment Guide
## Quick Start
### 1. Build and Run with Docker Compose
```bash
# Build the Docker image
docker-compose build
# Start the container
docker-compose up -d
# View logs
docker-compose logs -f
# Stop the container
docker-compose down
```
The application will be available at `http://localhost:5000`
Default credentials:
- Username: `admin`
- Password: `admin123`
### 2. Build Docker Image Only
```bash
# Build the image
docker build -t digiserver-v2:latest .
# Run the container
docker run -d \
-p 5000:5000 \
-v $(pwd)/instance:/app/instance \
-v $(pwd)/app/static/uploads:/app/app/static/uploads \
--name digiserver \
digiserver-v2:latest
```
## Configuration
### Environment Variables
Create a `.env` file based on `.env.example`:
```bash
cp .env.example .env
```
Edit the `.env` file to set your configuration:
- `SECRET_KEY`: Change to a random secret key
- `FLASK_ENV`: Set to `production` for production deployments
### Persistent Data
The following directories are mounted as volumes:
- `./instance`: Database storage
- `./app/static/uploads`: Uploaded media files
These persist even when containers are recreated.
## Production Deployment
### 1. Using Docker Compose (Recommended)
```bash
# Create .env file with production settings
cp .env.example .env
nano .env # Edit with your settings
# Start in production mode
docker-compose up -d
```
### 2. Behind a Reverse Proxy (Nginx/Traefik)
Example Nginx configuration:
```nginx
server {
listen 80;
server_name yourdomain.com;
location / {
proxy_pass http://localhost:5000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# For large file uploads
client_max_body_size 100M;
}
}
```
### 3. Enable Redis Caching (Optional)
Uncomment the Redis service in `docker-compose.yml`:
```yaml
redis:
image: redis:7-alpine
container_name: digiserver-redis
restart: unless-stopped
volumes:
- redis-data:/data
volumes:
redis-data:
```
Update `.env`:
```
REDIS_URL=redis://redis:6379/0
```
## Backup
### Database Backup
```bash
# Backup database
docker exec digiserver tar -czf /tmp/backup.tar.gz /app/instance
docker cp digiserver:/tmp/backup.tar.gz ./backup-$(date +%Y%m%d).tar.gz
```
### Full Backup (Database + Uploads)
```bash
# Backup everything
tar -czf digiserver-backup-$(date +%Y%m%d).tar.gz instance/ app/static/uploads/
```
## Maintenance
### View Logs
```bash
# All logs
docker-compose logs -f
# Last 100 lines
docker-compose logs --tail=100
# Specific service
docker-compose logs -f digiserver
```
### Update Application
```bash
# Pull latest code
git pull
# Rebuild and restart
docker-compose down
docker-compose build
docker-compose up -d
```
### Shell Access
```bash
# Access container shell
docker-compose exec digiserver bash
# Or with docker directly
docker exec -it digiserver bash
```
## Troubleshooting
### Port Already in Use
Change the port mapping in `docker-compose.yml`:
```yaml
ports:
- "8080:5000" # Change 8080 to your desired port
```
### Permission Issues
Ensure the volumes have correct permissions:
```bash
sudo chown -R 1000:1000 instance/ app/static/uploads/
```
### Container Won't Start
Check logs:
```bash
docker-compose logs digiserver
```
### Reset Database
```bash
# Stop containers
docker-compose down
# Remove database
rm instance/*.db
# Start fresh
docker-compose up -d
```
## System Requirements
- Docker 20.10+
- Docker Compose 2.0+
- 2GB RAM minimum
- 10GB disk space for media files
## Security Recommendations
1. **Change default credentials** immediately after first login
2. **Set a strong SECRET_KEY** in `.env`
3. **Use HTTPS** with a reverse proxy in production
4. **Regular backups** of database and uploads
5. **Update regularly** to get security patches
6. **Restrict network access** using firewall rules
7. **Monitor logs** for suspicious activity
## Performance Tuning
### Adjust Workers
Edit `Dockerfile` CMD line:
```dockerfile
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "8", "--timeout", "120", "app.app:create_app()"]
```
### Resource Limits
Add to `docker-compose.yml`:
```yaml
services:
digiserver:
# ... existing config ...
deploy:
resources:
limits:
cpus: '2'
memory: 2G
reservations:
cpus: '1'
memory: 1G
```

50
Dockerfile Normal file
View File

@@ -0,0 +1,50 @@
# Use Python 3.13 slim image
FROM python:3.13-slim
# Set working directory
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y \
poppler-utils \
libreoffice \
ffmpeg \
libmagic1 \
&& rm -rf /var/lib/apt/lists/*
# Copy requirements first for better caching
COPY requirements.txt .
# Install Python dependencies
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY . .
# Copy and set permissions for entrypoint script
COPY docker-entrypoint.sh /docker-entrypoint.sh
RUN chmod +x /docker-entrypoint.sh
# Create directories for uploads and database
RUN mkdir -p app/static/uploads instance
# Set environment variables
ENV FLASK_APP=app.app:create_app
ENV PYTHONUNBUFFERED=1
ENV FLASK_ENV=production
# Expose port
EXPOSE 5000
# Create a non-root user
RUN useradd -m -u 1000 appuser && \
chown -R appuser:appuser /app /docker-entrypoint.sh
USER appuser
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:5000/').read()" || exit 1
# Run the application via entrypoint
ENTRYPOINT ["/docker-entrypoint.sh"]

269
README.md Normal file
View File

@@ -0,0 +1,269 @@
# DigiServer v2
Digital Signage Management System - A modern Flask-based application for managing content playlists across multiple display screens.
## Features
- 📺 **Multi-Player Management** - Control multiple display screens from one interface
- 🎬 **Playlist System** - Create and manage content playlists with drag-and-drop reordering
- 📁 **Media Library** - Upload and organize images, videos, PDFs, and presentations
- 📄 **PDF to Image Conversion** - Automatic conversion of PDF pages to Full HD images (300 DPI)
- 📊 **PowerPoint Support** - Convert PPTX slides to images automatically
- 🖼️ **Live Preview** - Real-time content preview for each player
-**Real-time Updates** - Players automatically sync with playlist changes
- 🌓 **Dark Mode** - Full dark mode support across all interfaces
- 🗑️ **Media Management** - Clean up unused media files with leftover media manager
- 🔒 **User Authentication** - Secure admin access with role-based permissions
## Quick Start
### Option 1: Docker (Recommended)
```bash
# Quick start with Docker
./docker-start.sh
```
Access at: `http://localhost:5000`
Default credentials: `admin` / `admin123`
See [DOCKER.md](DOCKER.md) for detailed Docker documentation.
### Option 2: Manual Installation
#### Prerequisites
- Python 3.13+
- LibreOffice (for PPTX conversion)
- Poppler Utils (for PDF conversion)
- FFmpeg (for video processing)
#### Installation
```bash
# Install system dependencies (Debian/Ubuntu)
sudo apt-get update
sudo apt-get install -y poppler-utils libreoffice ffmpeg libmagic1
# Create virtual environment
python3 -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
# Install Python dependencies
pip install -r requirements.txt
# Initialize database
python -c "
from app.app import create_app
from app.extensions import db, bcrypt
from app.models import User
app = create_app()
with app.app_context():
db.create_all()
hashed = bcrypt.generate_password_hash('admin123').decode('utf-8')
admin = User(username='admin', password=hashed, role='admin')
db.session.add(admin)
db.session.commit()
print('Admin user created')
"
# Run development server
./run_dev.sh
```
Access at: `http://localhost:5000`
## Deployment
### Docker Deployment
```bash
# Build and run with Docker Compose
docker-compose up -d
# View logs
docker-compose logs -f
# Stop
docker-compose down
```
### Production Deployment
For production, use:
- Gunicorn or uWSGI as WSGI server
- Nginx as reverse proxy
- Redis for caching (optional)
- PostgreSQL for larger deployments (optional)
See [DOCKER.md](DOCKER.md) for detailed deployment instructions.
## Usage
### 1. Create a Playlist
1. Navigate to **Playlist Management**
2. Fill in playlist details (name, orientation, description)
3. Click **Create Playlist**
### 2. Upload Media
1. Go to **Upload Media** page
2. Select files (images, videos, PDFs, PPTX)
3. Choose media type and duration
4. Select target playlist (optional)
5. Click **Upload**
**Supported Formats:**
- Images: JPG, PNG, GIF, BMP, WEBP
- Videos: MP4, AVI, MOV, MKV, WEBM
- Documents: PDF, PPT, PPTX
### 3. Manage Playlists
1. Open playlist management
2. Drag and drop to reorder content
3. Edit duration for each item
4. Remove unwanted items
5. Changes sync automatically to players
### 4. Assign to Players
1. Go to **Player Assignments**
2. Select playlist from dropdown for each player
3. View live preview to verify content
### 5. Clean Up Media
1. Navigate to **Admin****Manage Leftover Media**
2. Review unused files
3. Delete individual files or bulk delete by type
## Configuration
### Environment Variables
Create a `.env` file:
```env
FLASK_ENV=production
SECRET_KEY=your-random-secret-key
DATABASE_URL=sqlite:///instance/digiserver.db
```
### Upload Settings
Edit `app/config.py` to adjust:
- Upload folder location
- Maximum file size
- Allowed file extensions
## Project Structure
```
digiserver-v2/
├── app/
│ ├── blueprints/ # Route handlers
│ │ ├── admin.py # Admin panel routes
│ │ ├── content.py # Content management
│ │ ├── playlist.py # Playlist operations
│ │ └── players.py # Player management
│ ├── models/ # Database models
│ ├── templates/ # HTML templates
│ ├── static/ # CSS, JS, uploads
│ └── utils/ # Helper functions
├── instance/ # Database storage
├── Dockerfile # Docker configuration
├── docker-compose.yml # Docker Compose config
└── requirements.txt # Python dependencies
```
## API Endpoints
### Player API
- `GET /api/playlist/<player_id>` - Get player playlist
- `POST /api/players/<player_id>/heartbeat` - Send heartbeat
### Admin API
- `POST /playlist/<player_id>/update-duration/<content_id>` - Update content duration
- `POST /playlist/<player_id>/reorder` - Reorder playlist items
## Troubleshooting
### PDF Conversion Fails
Ensure poppler-utils is installed:
```bash
sudo apt-get install poppler-utils
```
### PPTX Conversion Fails
Install LibreOffice:
```bash
sudo apt-get install libreoffice
```
### Upload Fails
Check folder permissions:
```bash
chmod -R 755 app/static/uploads
```
### Database Issues
Reset database:
```bash
rm instance/*.db
# Then reinitialize (see Installation)
```
## Development
### Running Tests
```bash
pytest
```
### Code Formatting
```bash
black app/
flake8 app/
```
### Database Migrations
```bash
flask db migrate -m "Description"
flask db upgrade
```
## Contributing
1. Fork the repository
2. Create a feature branch
3. Make your changes
4. Test thoroughly
5. Submit a pull request
## License
This project is proprietary software. All rights reserved.
## Support
For issues and questions:
- Check [DOCKER.md](DOCKER.md) for deployment help
- Review troubleshooting section
- Check application logs
## Version History
- **v2.0** - Complete rewrite with playlist-centric architecture
- PDF to image conversion (300 DPI)
- PPTX slide conversion
- Leftover media management
- Enhanced dark mode
- Duration editing for all content types
---
Built with ❤️ using Flask, SQLAlchemy, and modern web technologies

View File

@@ -308,12 +308,11 @@ def process_pdf_file(filepath: str, filename: str) -> tuple[bool, str]:
log_action('info', f'Converting PDF to images: {filename}')
# Convert PDF pages to images at Full HD resolution
# Convert PDF pages to images at high DPI for quality
images = convert_from_path(
filepath,
dpi=150, # Good quality for Full HD display
fmt='png',
size=(1920, 1080) # Direct Full HD output
dpi=300, # 300 DPI for sharp rendering
fmt='png'
)
if not images:
@@ -323,18 +322,34 @@ def process_pdf_file(filepath: str, filename: str) -> tuple[bool, str]:
base_filename = Path(filename).stem
upload_folder = os.path.dirname(filepath)
# Save each page directly as PNG
# Save each page with proper aspect ratio preservation
converted_files = []
for idx, image in enumerate(images, start=1):
# Create filename for this page
page_filename = f"{base_filename}_page{idx:03d}.png"
page_filepath = os.path.join(upload_folder, page_filename)
# Save the image directly without additional optimization
image.save(page_filepath, 'PNG', optimize=True)
# Determine orientation and resize maintaining aspect ratio
width, height = image.size
is_portrait = height > width
# Define Full HD dimensions based on orientation
if is_portrait:
# Portrait: max height 1920, max width 1080 (rotated Full HD)
max_size = (1080, 1920)
else:
# Landscape: max width 1920, max height 1080 (standard Full HD)
max_size = (1920, 1080)
# Resize maintaining aspect ratio (thumbnail maintains ratio)
from PIL import Image as PILImage
image.thumbnail(max_size, PILImage.Resampling.LANCZOS)
# Save the optimized image
image.save(page_filepath, 'PNG', optimize=True, quality=95)
converted_files.append((page_filepath, page_filename))
log_action('info', f'Converted PDF page {idx}/{len(images)}: {page_filename}')
log_action('info', f'Converted PDF page {idx}/{len(images)} ({width}x{height} -> {image.size[0]}x{image.size[1]}): {page_filename}')
log_action('info', f'PDF converted successfully: {len(images)} pages from {filename}')

View File

@@ -205,6 +205,14 @@
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
}
.media-thumbnail {
background: #f0f0f0;
}
body.dark-mode .media-thumbnail {
background: #1a202c;
}
.media-icon {
font-size: 48px;
margin-bottom: 10px;
@@ -334,55 +342,57 @@
<div class="card-header">
<h2 style="display: flex; align-items: center; gap: 0.5rem;">
<img src="{{ url_for('static', filename='icons/upload.svg') }}" alt="" style="width: 24px; height: 24px; filter: brightness(0) invert(1);">
Upload Media
Media Library
</h2>
</div>
<div style="text-align: center; padding: 40px 20px;">
<img src="{{ url_for('static', filename='icons/upload.svg') }}" alt="" style="width: 96px; height: 96px; opacity: 0.5; margin-bottom: 20px;">
<h3 style="margin-bottom: 15px;">Upload Media Files</h3>
<p style="color: #6c757d; margin-bottom: 25px;">
Upload images, videos, and PDFs to your media library.<br>
Assign them to playlists during or after upload.
</p>
<a href="{{ url_for('content.upload_media_page') }}" class="btn btn-success" style="padding: 15px 40px; font-size: 16px; display: inline-flex; align-items: center; gap: 0.5rem;">
<img src="{{ url_for('static', filename='icons/upload.svg') }}" alt="" style="width: 18px; height: 18px; filter: brightness(0) invert(1);">
Go to Upload Page
<!-- Compact Upload Section -->
<div style="text-align: center; padding: 20px; background: #f8f9fa; border-radius: 8px; margin-bottom: 20px;">
<a href="{{ url_for('content.upload_media_page') }}" class="btn btn-success" style="display: inline-flex; align-items: center; gap: 0.5rem;">
<img src="{{ url_for('static', filename='icons/upload.svg') }}" alt="" style="width: 16px; height: 16px; filter: brightness(0) invert(1);">
Upload New Media
</a>
</div>
<!-- Media Library Preview -->
<hr style="margin: 25px 0;">
<h3 style="margin-bottom: 15px;">Media Library ({{ media_files|length }} files)</h3>
<div class="media-library">
<!-- Media Library with Thumbnails -->
<h3 style="margin-bottom: 15px; display: flex; align-items: center; justify-content: space-between;">
<span>📚 Available Media ({{ media_files|length }})</span>
</h3>
<div class="media-library" style="max-height: 500px; overflow-y: auto;">
{% if media_files %}
{% for media in media_files[:12] %}
{% for media in media_files %}
<div class="media-item" title="{{ media.filename }}">
<div class="media-icon">
{% if media.content_type == 'image' %}
<img src="{{ url_for('static', filename='icons/info.svg') }}" alt="Image" style="width: 48px; height: 48px; opacity: 0.5;">
{% elif media.content_type == 'video' %}
<img src="{{ url_for('static', filename='icons/monitor.svg') }}" alt="Video" style="width: 48px; height: 48px; opacity: 0.5;">
{% elif media.content_type == 'pdf' %}
<img src="{{ url_for('static', filename='icons/info.svg') }}" alt="PDF" style="width: 48px; height: 48px; opacity: 0.5;">
{% else %}
<img src="{{ url_for('static', filename='icons/info.svg') }}" alt="File" style="width: 48px; height: 48px; opacity: 0.5;">
{% endif %}
</div>
<div class="media-name">{{ media.filename[:20] }}...</div>
{% if media.content_type == 'image' %}
<div class="media-thumbnail" style="width: 100%; height: 100px; overflow: hidden; border-radius: 6px; margin-bottom: 8px; background: #f0f0f0; display: flex; align-items: center; justify-content: center;">
<img src="{{ url_for('static', filename='uploads/' + media.filename) }}"
alt="{{ media.filename }}"
style="max-width: 100%; max-height: 100%; object-fit: cover;"
onerror="this.style.display='none'; this.parentElement.innerHTML='<span style=\'font-size: 48px;\'>📷</span>'">
</div>
{% elif media.content_type == 'video' %}
<div class="media-icon" style="height: 100px; display: flex; align-items: center; justify-content: center; background: #f0f0f0; border-radius: 6px; margin-bottom: 8px;">
🎥
</div>
{% elif media.content_type == 'pdf' %}
<div class="media-icon" style="height: 100px; display: flex; align-items: center; justify-content: center; background: #f0f0f0; border-radius: 6px; margin-bottom: 8px;">
📄
</div>
{% else %}
<div class="media-icon" style="height: 100px; display: flex; align-items: center; justify-content: center; background: #f0f0f0; border-radius: 6px; margin-bottom: 8px;">
📁
</div>
{% endif %}
<div class="media-name" style="font-size: 11px; line-height: 1.3;">{{ media.filename[:25] }}{% if media.filename|length > 25 %}...{% endif %}</div>
<div style="font-size: 10px; color: #999; margin-top: 4px;">{{ "%.1f"|format(media.file_size_mb) }} MB</div>
</div>
{% endfor %}
{% else %}
<div style="text-align: center; padding: 20px; color: #999;">
<div style="text-align: center; padding: 40px; color: #999; grid-column: 1 / -1;">
<div style="font-size: 48px; margin-bottom: 10px;">📭</div>
<p>No media files yet. Upload your first file!</p>
</div>
{% endif %}
</div>
{% if media_files|length > 12 %}
<p style="text-align: center; margin-top: 15px; color: #999;">
+ {{ media_files|length - 12 }} more files
</p>
{% endif %}
</div>
</div>

32
docker-compose.yml Normal file
View File

@@ -0,0 +1,32 @@
version: '3.8'
services:
digiserver:
build: .
container_name: digiserver-v2
ports:
- "5000:5000"
volumes:
- ./instance:/app/instance
- ./app/static/uploads:/app/app/static/uploads
environment:
- FLASK_ENV=production
- SECRET_KEY=${SECRET_KEY:-your-secret-key-change-this}
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:5000/"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
# Optional: Redis for caching (uncomment if needed)
# redis:
# image: redis:7-alpine
# container_name: digiserver-redis
# restart: unless-stopped
# volumes:
# - redis-data:/data
# volumes:
# redis-data:

44
docker-entrypoint.sh Executable file
View File

@@ -0,0 +1,44 @@
#!/bin/bash
set -e
echo "Starting DigiServer v2..."
# Create necessary directories
mkdir -p /app/instance
mkdir -p /app/app/static/uploads
# Initialize database if it doesn't exist
if [ ! -f /app/instance/digiserver.db ]; then
echo "Initializing database..."
python -c "
from app.app import create_app
from app.extensions import db, bcrypt
from app.models import User
app = create_app()
with app.app_context():
db.create_all()
# Create admin user
admin = User.query.filter_by(username='admin').first()
if not admin:
hashed = bcrypt.generate_password_hash('admin123').decode('utf-8')
admin = User(username='admin', password=hashed, role='admin')
db.session.add(admin)
db.session.commit()
print('✅ Admin user created (admin/admin123)')
else:
print('✅ Admin user already exists')
"
echo "Database initialized!"
fi
# Start the application
echo "Starting Gunicorn..."
exec gunicorn \
--bind 0.0.0.0:5000 \
--workers 4 \
--timeout 120 \
--access-logfile - \
--error-logfile - \
"app.app:create_app()"

69
docker-start.sh Executable file
View File

@@ -0,0 +1,69 @@
#!/bin/bash
echo "🚀 DigiServer v2 - Docker Quick Start"
echo "====================================="
echo ""
# Check if Docker is installed
if ! command -v docker &> /dev/null; then
echo "❌ Docker is not installed. Please install Docker first."
exit 1
fi
# Check if Docker Compose is installed
if ! command -v docker-compose &> /dev/null; then
echo "❌ Docker Compose is not installed. Please install Docker Compose first."
exit 1
fi
# Create .env file if it doesn't exist
if [ ! -f .env ]; then
echo "📝 Creating .env file..."
cp .env.example .env
# Generate random secret key
SECRET_KEY=$(openssl rand -base64 32)
sed -i "s/change-this-to-a-random-secret-key/$SECRET_KEY/" .env
echo "✅ Created .env with generated SECRET_KEY"
fi
# Create required directories
echo "📁 Creating required directories..."
mkdir -p instance app/static/uploads
echo "✅ Directories created"
echo ""
echo "🔨 Building Docker image..."
docker-compose build
echo ""
echo "🚀 Starting DigiServer v2..."
docker-compose up -d
echo ""
echo "⏳ Waiting for application to start..."
sleep 5
# Check if container is running
if docker-compose ps | grep -q "Up"; then
echo ""
echo "✅ DigiServer v2 is running!"
echo ""
echo "📍 Access the application at: http://localhost:5000"
echo ""
echo "👤 Default credentials:"
echo " Username: admin"
echo " Password: admin123"
echo ""
echo "📋 Useful commands:"
echo " View logs: docker-compose logs -f"
echo " Stop: docker-compose down"
echo " Restart: docker-compose restart"
echo " Shell access: docker-compose exec digiserver bash"
echo ""
echo "⚠️ IMPORTANT: Change the admin password after first login!"
else
echo ""
echo "❌ Failed to start DigiServer v2"
echo " Check logs with: docker-compose logs"
fi