CLAUDE: Add comprehensive Docker deployment infrastructure
Implements production-ready Docker setup with multi-stage builds and separate development/production configurations. New Files: - Dockerfile: Multi-stage build with Python 3.13 * Builder stage: Compiles dependencies with build tools * Runtime stage: Minimal image (~150-200MB) with non-root user * Health checks and security hardening - docker-compose.yml: Production config (pulls from Docker Hub) * Image: manticorum67/major-domo-discordapp:latest * Resource limits: 512MB RAM, 1 CPU * Volumes: /app/data (ro), /app/logs (rw) - docker-compose.dev.yml: Development config (builds locally) * Higher resource limits: 1GB RAM, 2 CPU * DEBUG log level by default - .dockerignore: Excludes unnecessary files from build context - build-and-push.sh: Interactive build/push script for Docker Hub - DOCKER.md: Comprehensive deployment guide (13K) - BUILD_AND_PUSH.md: Docker Hub build/push guide (7.7K) Configuration Updates: - config.py: Updated sheets_credentials_path to /app/data location - requirements.txt: Pinned all package versions for reproducibility - .env.example: Added Docker-specific configuration Key Features: - Multi-stage build for optimized image size - Non-root user (botuser, UID 1000) for security - Separate dev/prod compose files - Volume mounts for persistence (/app/data, /app/logs) - Health checks and automatic restarts - Resource limits and log rotation - Docker Hub integration for production deployments Docker Hub Repository: manticorum67/major-domo-discordapp 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
2409c27c1d
commit
5924249481
76
.dockerignore
Normal file
76
.dockerignore
Normal file
@ -0,0 +1,76 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# Virtual environments
|
||||
venv/
|
||||
.venv/
|
||||
ENV/
|
||||
env/
|
||||
|
||||
# IDEs
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Testing
|
||||
.pytest_cache/
|
||||
.coverage
|
||||
htmlcov/
|
||||
.tox/
|
||||
tests/
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Data files
|
||||
data/
|
||||
*.json
|
||||
|
||||
# Git
|
||||
.git/
|
||||
.gitignore
|
||||
|
||||
# Docker
|
||||
Dockerfile
|
||||
docker-compose*
|
||||
.dockerignore
|
||||
|
||||
# Documentation
|
||||
.claude/
|
||||
*.md
|
||||
docs/
|
||||
|
||||
# CI/CD
|
||||
.github/
|
||||
.gitlab-ci.yml
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
371
BUILD_AND_PUSH.md
Normal file
371
BUILD_AND_PUSH.md
Normal file
@ -0,0 +1,371 @@
|
||||
# Building and Pushing to Docker Hub
|
||||
|
||||
This guide covers building the Docker image and pushing it to Docker Hub for production deployment.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Docker installed and running
|
||||
- Docker Hub account (username: `manticorum67`)
|
||||
- Write access to `manticorum67/major-domo-discordapp` repository
|
||||
|
||||
## Docker Hub Repository
|
||||
|
||||
**Repository**: `manticorum67/major-domo-discordapp`
|
||||
**URL**: https://hub.docker.com/r/manticorum67/major-domo-discordapp
|
||||
|
||||
## Login to Docker Hub
|
||||
|
||||
```bash
|
||||
# Login to Docker Hub
|
||||
docker login
|
||||
|
||||
# Enter your username: manticorum67
|
||||
# Enter your password/token: [your-password-or-token]
|
||||
```
|
||||
|
||||
## Build and Push Workflow
|
||||
|
||||
### 1. Tag the Release
|
||||
|
||||
```bash
|
||||
# Determine version number (use semantic versioning)
|
||||
VERSION="2.0.0"
|
||||
|
||||
# Create git tag (optional but recommended)
|
||||
git tag -a "v${VERSION}" -m "Release v${VERSION}"
|
||||
git push origin "v${VERSION}"
|
||||
```
|
||||
|
||||
### 2. Build the Image
|
||||
|
||||
```bash
|
||||
# Build for production
|
||||
docker build -t manticorum67/major-domo-discordapp:latest .
|
||||
|
||||
# Build with version tag
|
||||
docker build -t manticorum67/major-domo-discordapp:${VERSION} .
|
||||
|
||||
# Or build both at once
|
||||
docker build \
|
||||
-t manticorum67/major-domo-discordapp:latest \
|
||||
-t manticorum67/major-domo-discordapp:${VERSION} \
|
||||
.
|
||||
```
|
||||
|
||||
### 3. Test the Image Locally
|
||||
|
||||
```bash
|
||||
# Test with docker run
|
||||
docker run --rm \
|
||||
--env-file .env \
|
||||
-v $(pwd)/data:/data:ro \
|
||||
-v $(pwd)/logs:/logs:rw \
|
||||
manticorum67/major-domo-discordapp:latest
|
||||
|
||||
# Or test with docker-compose (development)
|
||||
docker-compose -f docker-compose.dev.yml up
|
||||
```
|
||||
|
||||
### 4. Push to Docker Hub
|
||||
|
||||
```bash
|
||||
# Push latest tag
|
||||
docker push manticorum67/major-domo-discordapp:latest
|
||||
|
||||
# Push version tag
|
||||
docker push manticorum67/major-domo-discordapp:${VERSION}
|
||||
|
||||
# Or push all tags
|
||||
docker push manticorum67/major-domo-discordapp --all-tags
|
||||
```
|
||||
|
||||
## Complete Build and Push Script
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# build-and-push.sh
|
||||
|
||||
set -e # Exit on error
|
||||
|
||||
# Configuration
|
||||
VERSION="${1:-latest}" # Use argument or default to 'latest'
|
||||
DOCKER_REPO="manticorum67/major-domo-discordapp"
|
||||
|
||||
echo "🔨 Building Docker image..."
|
||||
echo "Version: ${VERSION}"
|
||||
echo "Repository: ${DOCKER_REPO}"
|
||||
echo ""
|
||||
|
||||
# Build image with both tags
|
||||
docker build \
|
||||
-t ${DOCKER_REPO}:latest \
|
||||
-t ${DOCKER_REPO}:${VERSION} \
|
||||
.
|
||||
|
||||
echo ""
|
||||
echo "✅ Build complete!"
|
||||
echo ""
|
||||
echo "📤 Pushing to Docker Hub..."
|
||||
|
||||
# Push both tags
|
||||
docker push ${DOCKER_REPO}:latest
|
||||
docker push ${DOCKER_REPO}:${VERSION}
|
||||
|
||||
echo ""
|
||||
echo "✅ Push complete!"
|
||||
echo ""
|
||||
echo "🎉 Image available at:"
|
||||
echo " docker pull ${DOCKER_REPO}:latest"
|
||||
echo " docker pull ${DOCKER_REPO}:${VERSION}"
|
||||
```
|
||||
|
||||
### Using the Build Script
|
||||
|
||||
```bash
|
||||
# Make script executable
|
||||
chmod +x build-and-push.sh
|
||||
|
||||
# Build and push with version
|
||||
./build-and-push.sh 2.0.0
|
||||
|
||||
# Build and push as latest only
|
||||
./build-and-push.sh
|
||||
```
|
||||
|
||||
## Multi-Platform Builds (Optional)
|
||||
|
||||
To build for multiple architectures (amd64, arm64):
|
||||
|
||||
```bash
|
||||
# Create a builder instance
|
||||
docker buildx create --name multiarch --use
|
||||
|
||||
# Build and push for multiple platforms
|
||||
docker buildx build \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
-t manticorum67/major-domo-discordapp:latest \
|
||||
-t manticorum67/major-domo-discordapp:${VERSION} \
|
||||
--push \
|
||||
.
|
||||
```
|
||||
|
||||
## Versioning Strategy
|
||||
|
||||
### Semantic Versioning
|
||||
|
||||
Use semantic versioning (MAJOR.MINOR.PATCH):
|
||||
|
||||
- **MAJOR**: Breaking changes
|
||||
- **MINOR**: New features (backwards compatible)
|
||||
- **PATCH**: Bug fixes
|
||||
|
||||
Examples:
|
||||
- `2.0.0` - Major release with scorecard submission
|
||||
- `2.1.0` - Added new command
|
||||
- `2.1.1` - Fixed bug in existing command
|
||||
|
||||
### Tagging Strategy
|
||||
|
||||
Always maintain these tags:
|
||||
|
||||
1. **`:latest`** - Most recent stable release
|
||||
2. **`:VERSION`** - Specific version (e.g., `2.0.0`)
|
||||
3. **`:MAJOR.MINOR`** - Minor version (e.g., `2.0`) - optional
|
||||
4. **`:MAJOR`** - Major version (e.g., `2`) - optional
|
||||
|
||||
### Example Tagging
|
||||
|
||||
```bash
|
||||
VERSION="2.0.0"
|
||||
|
||||
# Tag with all versions
|
||||
docker build \
|
||||
-t manticorum67/major-domo-discordapp:latest \
|
||||
-t manticorum67/major-domo-discordapp:2.0.0 \
|
||||
-t manticorum67/major-domo-discordapp:2.0 \
|
||||
-t manticorum67/major-domo-discordapp:2 \
|
||||
.
|
||||
|
||||
# Push all tags
|
||||
docker push manticorum67/major-domo-discordapp --all-tags
|
||||
```
|
||||
|
||||
## GitHub Actions (Optional)
|
||||
|
||||
Automate builds with GitHub Actions:
|
||||
|
||||
```yaml
|
||||
# .github/workflows/docker-build.yml
|
||||
name: Build and Push Docker Image
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Extract version
|
||||
id: version
|
||||
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: ./discord-app-v2
|
||||
push: true
|
||||
tags: |
|
||||
manticorum67/major-domo-discordapp:latest
|
||||
manticorum67/major-domo-discordapp:${{ steps.version.outputs.VERSION }}
|
||||
```
|
||||
|
||||
## Production Deployment
|
||||
|
||||
After pushing to Docker Hub, deploy on production:
|
||||
|
||||
```bash
|
||||
# On production server
|
||||
cd /path/to/discord-app-v2
|
||||
|
||||
# Pull latest image
|
||||
docker-compose pull
|
||||
|
||||
# Restart with new image
|
||||
docker-compose up -d
|
||||
|
||||
# Verify it's running
|
||||
docker-compose logs -f discord-bot
|
||||
```
|
||||
|
||||
## Rollback to Previous Version
|
||||
|
||||
If a release has issues:
|
||||
|
||||
```bash
|
||||
# Stop current version
|
||||
docker-compose down
|
||||
|
||||
# Edit docker-compose.yml to use specific version
|
||||
# Change: image: manticorum67/major-domo-discordapp:latest
|
||||
# To: image: manticorum67/major-domo-discordapp:2.0.0
|
||||
|
||||
# Pull and start old version
|
||||
docker-compose pull
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
Or use a specific version directly:
|
||||
|
||||
```bash
|
||||
docker-compose down
|
||||
|
||||
docker pull manticorum67/major-domo-discordapp:2.0.0
|
||||
|
||||
docker run -d \
|
||||
--name major-domo-discord-bot-v2 \
|
||||
--env-file .env \
|
||||
-v $(pwd)/data:/data:ro \
|
||||
-v $(pwd)/logs:/logs:rw \
|
||||
manticorum67/major-domo-discordapp:2.0.0
|
||||
```
|
||||
|
||||
## Image Size Optimization
|
||||
|
||||
The multi-stage build already optimizes size, but you can verify:
|
||||
|
||||
```bash
|
||||
# Check image size
|
||||
docker images manticorum67/major-domo-discordapp
|
||||
|
||||
# Expected size: ~150-200MB
|
||||
|
||||
# Inspect layers
|
||||
docker history manticorum67/major-domo-discordapp:latest
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Build Fails
|
||||
|
||||
```bash
|
||||
# Build with verbose output
|
||||
docker build --progress=plain -t manticorum67/major-domo-discordapp:latest .
|
||||
|
||||
# Check for errors in requirements.txt
|
||||
docker build --no-cache -t manticorum67/major-domo-discordapp:latest .
|
||||
```
|
||||
|
||||
### Push Fails
|
||||
|
||||
```bash
|
||||
# Check if logged in
|
||||
docker info | grep Username
|
||||
|
||||
# Re-login
|
||||
docker logout
|
||||
docker login
|
||||
|
||||
# Check repository permissions
|
||||
docker push manticorum67/major-domo-discordapp:latest
|
||||
```
|
||||
|
||||
### Image Won't Run
|
||||
|
||||
```bash
|
||||
# Test image interactively
|
||||
docker run -it --rm \
|
||||
--entrypoint /bin/bash \
|
||||
manticorum67/major-domo-discordapp:latest
|
||||
|
||||
# Inside container, check Python
|
||||
python --version
|
||||
pip list
|
||||
ls -la /app
|
||||
```
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
1. **Use Docker Hub Access Tokens** instead of password
|
||||
2. **Enable 2FA** on Docker Hub account
|
||||
3. **Scan images** for vulnerabilities:
|
||||
```bash
|
||||
docker scan manticorum67/major-domo-discordapp:latest
|
||||
```
|
||||
4. **Sign images** (optional):
|
||||
```bash
|
||||
docker trust sign manticorum67/major-domo-discordapp:latest
|
||||
```
|
||||
|
||||
## Cleanup
|
||||
|
||||
Remove old local images:
|
||||
|
||||
```bash
|
||||
# Remove dangling images
|
||||
docker image prune
|
||||
|
||||
# Remove all unused images
|
||||
docker image prune -a
|
||||
|
||||
# Remove specific version
|
||||
docker rmi manticorum67/major-domo-discordapp:1.0.0
|
||||
```
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- **Docker Hub**: https://hub.docker.com/r/manticorum67/major-domo-discordapp
|
||||
- **Docker Documentation**: https://docs.docker.com/
|
||||
- **Semantic Versioning**: https://semver.org/
|
||||
568
DOCKER.md
Normal file
568
DOCKER.md
Normal file
@ -0,0 +1,568 @@
|
||||
# Docker Deployment Guide
|
||||
|
||||
This guide covers deploying the Discord Bot v2.0 using Docker and Docker Compose.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Docker 20.10+ installed
|
||||
- Docker Compose 2.0+ installed
|
||||
- Google Sheets service account credentials JSON file
|
||||
- Access to the database API (running on a separate host)
|
||||
|
||||
## Deployment Modes
|
||||
|
||||
### Production (Recommended)
|
||||
Uses `docker-compose.yml` - pulls pre-built image from Docker Hub
|
||||
|
||||
### Development
|
||||
Uses `docker-compose.dev.yml` - builds image locally from source
|
||||
|
||||
---
|
||||
|
||||
## Quick Start (Production)
|
||||
|
||||
Deploy using pre-built image from Docker Hub:
|
||||
|
||||
### 1. Prepare Configuration
|
||||
|
||||
```bash
|
||||
# Copy the example environment file
|
||||
cp .env.example .env
|
||||
|
||||
# Edit .env with your actual values
|
||||
nano .env
|
||||
```
|
||||
|
||||
**Required environment variables:**
|
||||
- `BOT_TOKEN` - Your Discord bot token
|
||||
- `API_TOKEN` - Database API authentication token
|
||||
- `DB_URL` - Database API endpoint URL
|
||||
- `GUILD_ID` - Your Discord server ID
|
||||
|
||||
### 2. Prepare Data Directory
|
||||
|
||||
```bash
|
||||
# Create data directory for Google Sheets credentials
|
||||
mkdir -p data
|
||||
|
||||
# Copy your Google Sheets credentials file
|
||||
cp /path/to/your/credentials.json data/major-domo-service-creds.json
|
||||
|
||||
# Set proper permissions (read-only)
|
||||
chmod 444 data/major-domo-service-creds.json
|
||||
```
|
||||
|
||||
### 3. Create Logs Directory
|
||||
|
||||
```bash
|
||||
# Create logs directory (will be mounted as volume)
|
||||
mkdir -p logs
|
||||
```
|
||||
|
||||
### 4. Pull and Run
|
||||
|
||||
```bash
|
||||
# Pull latest image from Docker Hub
|
||||
docker-compose pull
|
||||
|
||||
# Start the bot
|
||||
docker-compose up -d
|
||||
|
||||
# View logs
|
||||
docker-compose logs -f discord-bot
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Development Setup
|
||||
|
||||
Build and run locally with source code:
|
||||
|
||||
### 1. Complete steps 1-3 from Production setup above
|
||||
|
||||
### 2. Build and Run
|
||||
|
||||
```bash
|
||||
# Build the Docker image locally
|
||||
docker-compose -f docker-compose.dev.yml build
|
||||
|
||||
# Start the bot
|
||||
docker-compose -f docker-compose.dev.yml up -d
|
||||
|
||||
# View logs
|
||||
docker-compose -f docker-compose.dev.yml logs -f discord-bot
|
||||
```
|
||||
|
||||
## Docker Commands
|
||||
|
||||
### Production Commands
|
||||
|
||||
```bash
|
||||
# Pull latest image from Docker Hub
|
||||
docker-compose pull
|
||||
|
||||
# Start in detached mode
|
||||
docker-compose up -d
|
||||
|
||||
# Start in foreground (see logs)
|
||||
docker-compose up
|
||||
|
||||
# Restart the bot
|
||||
docker-compose restart discord-bot
|
||||
|
||||
# Stop the bot
|
||||
docker-compose stop discord-bot
|
||||
|
||||
# Stop and remove container
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
### Development Commands
|
||||
|
||||
```bash
|
||||
# Build the image locally
|
||||
docker-compose -f docker-compose.dev.yml build
|
||||
|
||||
# Build without cache (force rebuild)
|
||||
docker-compose -f docker-compose.dev.yml build --no-cache
|
||||
|
||||
# Start in detached mode
|
||||
docker-compose -f docker-compose.dev.yml up -d
|
||||
|
||||
# Start and rebuild if needed
|
||||
docker-compose -f docker-compose.dev.yml up -d --build
|
||||
|
||||
# Restart the bot
|
||||
docker-compose -f docker-compose.dev.yml restart discord-bot
|
||||
|
||||
# Stop and remove
|
||||
docker-compose -f docker-compose.dev.yml down
|
||||
```
|
||||
|
||||
### Monitoring Commands
|
||||
|
||||
```bash
|
||||
# View logs (follow mode)
|
||||
docker-compose logs -f discord-bot
|
||||
|
||||
# View last 100 lines of logs
|
||||
docker-compose logs --tail=100 discord-bot
|
||||
|
||||
# Check container status
|
||||
docker-compose ps
|
||||
|
||||
# Check resource usage
|
||||
docker stats major-domo-discord-bot-v2
|
||||
|
||||
# Execute commands inside container
|
||||
docker-compose exec discord-bot bash
|
||||
|
||||
# View bot process
|
||||
docker-compose exec discord-bot ps aux
|
||||
```
|
||||
|
||||
### Maintenance Commands
|
||||
|
||||
```bash
|
||||
# Pull latest code and restart
|
||||
git pull
|
||||
docker-compose up -d --build
|
||||
|
||||
# Clear logs
|
||||
docker-compose exec discord-bot sh -c "rm -rf /logs/*.log"
|
||||
|
||||
# Restart after configuration changes
|
||||
docker-compose down && docker-compose up -d
|
||||
|
||||
# View container health status
|
||||
docker inspect --format='{{.State.Health.Status}}' major-domo-discord-bot-v2
|
||||
```
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
discord-app-v2/
|
||||
├── Dockerfile # Multi-stage build configuration
|
||||
├── docker-compose.yml # Production: pulls from Docker Hub
|
||||
├── docker-compose.dev.yml # Development: builds locally
|
||||
├── .dockerignore # Files to exclude from image
|
||||
├── .env # Environment configuration (not in git)
|
||||
├── .env.example # Environment template
|
||||
├── DOCKER.md # This deployment guide
|
||||
├── BUILD_AND_PUSH.md # Guide for pushing to Docker Hub
|
||||
├── data/ # Google Sheets credentials (mounted volume)
|
||||
│ └── major-domo-service-creds.json
|
||||
├── logs/ # Log files (mounted volume)
|
||||
│ ├── discord_bot_v2.log
|
||||
│ └── discord_bot_v2.json
|
||||
└── ... (application code)
|
||||
```
|
||||
|
||||
## Docker Hub Repository
|
||||
|
||||
**Repository**: `manticorum67/major-domo-discordapp`
|
||||
**URL**: https://hub.docker.com/r/manticorum67/major-domo-discordapp
|
||||
|
||||
Production deployments pull from this repository. See `BUILD_AND_PUSH.md` for instructions on building and pushing new versions.
|
||||
|
||||
## Multi-Stage Build
|
||||
|
||||
The Dockerfile uses a multi-stage build for optimization:
|
||||
|
||||
### Stage 1: Builder
|
||||
- Based on `python:3.13-slim`
|
||||
- Installs build dependencies (gcc, g++)
|
||||
- Compiles Python packages with C extensions
|
||||
- Creates `.local` directory with all dependencies
|
||||
|
||||
### Stage 2: Runtime
|
||||
- Based on `python:3.13-slim`
|
||||
- Only includes runtime dependencies
|
||||
- Copies compiled packages from builder
|
||||
- Runs as non-root user (`botuser`)
|
||||
- Final image size: ~150-200MB (vs ~500MB+ single-stage)
|
||||
|
||||
## Security Features
|
||||
|
||||
### Non-Root User
|
||||
The bot runs as `botuser` (UID 1000) with restricted permissions:
|
||||
```dockerfile
|
||||
RUN groupadd -r botuser && \
|
||||
useradd -r -g botuser -u 1000 -m -s /bin/bash botuser
|
||||
USER botuser
|
||||
```
|
||||
|
||||
### Read-Only Credentials
|
||||
Mount credentials as read-only:
|
||||
```yaml
|
||||
volumes:
|
||||
- ./data:/data:ro # ro = read-only
|
||||
```
|
||||
|
||||
### Resource Limits
|
||||
Default resource limits in docker-compose.yml:
|
||||
- CPU: 1.0 cores max, 0.25 cores reserved
|
||||
- Memory: 512MB max, 256MB reserved
|
||||
|
||||
Adjust based on your server capacity:
|
||||
```yaml
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
cpus: '2.0' # Increase for heavy workloads
|
||||
memory: 1G # Increase if needed
|
||||
```
|
||||
|
||||
## Volume Mounts
|
||||
|
||||
### Data Volume (Read-Only)
|
||||
Contains Google Sheets credentials:
|
||||
```yaml
|
||||
volumes:
|
||||
- ${SHEETS_CREDENTIALS_HOST_PATH:-./data}:/data:ro
|
||||
```
|
||||
|
||||
### Logs Volume (Read-Write)
|
||||
Persistent log storage:
|
||||
```yaml
|
||||
volumes:
|
||||
- ${LOGS_HOST_PATH:-./logs}:/logs:rw
|
||||
```
|
||||
|
||||
### Development Mode
|
||||
Mount source code for live development:
|
||||
```yaml
|
||||
volumes:
|
||||
- .:/app:ro # Uncomment in docker-compose.yml
|
||||
```
|
||||
|
||||
## Health Checks
|
||||
|
||||
The bot includes a health check that runs every 60 seconds:
|
||||
```dockerfile
|
||||
HEALTHCHECK --interval=60s --timeout=10s --start-period=30s --retries=3 \
|
||||
CMD python -c "import sys; sys.exit(0)" || exit 1
|
||||
```
|
||||
|
||||
Check health status:
|
||||
```bash
|
||||
docker inspect --format='{{.State.Health.Status}}' major-domo-discord-bot-v2
|
||||
```
|
||||
|
||||
## Logging
|
||||
|
||||
### Container Logs
|
||||
Docker captures stdout/stderr:
|
||||
```bash
|
||||
docker-compose logs -f discord-bot
|
||||
```
|
||||
|
||||
### Application Logs
|
||||
Persistent logs in mounted volume:
|
||||
- `/logs/discord_bot_v2.log` - Human-readable logs
|
||||
- `/logs/discord_bot_v2.json` - Structured JSON logs
|
||||
|
||||
### Log Rotation
|
||||
Docker manages log rotation:
|
||||
```yaml
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "10m" # Max size per file
|
||||
max-file: "3" # Keep 3 files
|
||||
```
|
||||
|
||||
## Networking
|
||||
|
||||
The bot connects outbound to:
|
||||
- Discord API (discord.com)
|
||||
- Database API (configured via `DB_URL`)
|
||||
- Google Sheets API (sheets.googleapis.com)
|
||||
|
||||
No inbound ports are exposed (bot initiates all connections).
|
||||
|
||||
### Custom Network
|
||||
The compose file creates a bridge network:
|
||||
```yaml
|
||||
networks:
|
||||
major-domo-network:
|
||||
driver: bridge
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Bot Won't Start
|
||||
|
||||
1. **Check logs:**
|
||||
```bash
|
||||
docker-compose logs discord-bot
|
||||
```
|
||||
|
||||
2. **Verify environment variables:**
|
||||
```bash
|
||||
docker-compose exec discord-bot env | grep BOT_TOKEN
|
||||
```
|
||||
|
||||
3. **Check credentials file:**
|
||||
```bash
|
||||
docker-compose exec discord-bot ls -la /data/
|
||||
```
|
||||
|
||||
### Permission Errors
|
||||
|
||||
If you see permission errors accessing `/data` or `/logs`:
|
||||
```bash
|
||||
# Fix data directory permissions
|
||||
chmod -R 755 data/
|
||||
chmod 444 data/major-domo-service-creds.json
|
||||
|
||||
# Fix logs directory permissions
|
||||
chmod -R 755 logs/
|
||||
```
|
||||
|
||||
### Database Connection Issues
|
||||
|
||||
Test database connectivity:
|
||||
```bash
|
||||
docker-compose exec discord-bot python -c "
|
||||
import aiohttp
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
async def test():
|
||||
url = os.getenv('DB_URL')
|
||||
token = os.getenv('API_TOKEN')
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(f'{url}/health',
|
||||
headers={'Authorization': f'Bearer {token}'}) as resp:
|
||||
print(f'Status: {resp.status}')
|
||||
print(await resp.text())
|
||||
|
||||
asyncio.run(test())
|
||||
"
|
||||
```
|
||||
|
||||
### High Memory Usage
|
||||
|
||||
If the bot uses too much memory:
|
||||
|
||||
1. **Check current usage:**
|
||||
```bash
|
||||
docker stats major-domo-discord-bot-v2
|
||||
```
|
||||
|
||||
2. **Increase memory limit:**
|
||||
```yaml
|
||||
# In docker-compose.yml
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 1G # Increase from 512M
|
||||
```
|
||||
|
||||
3. **Restart with new limits:**
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### Container Keeps Restarting
|
||||
|
||||
Check exit code and error:
|
||||
```bash
|
||||
docker-compose ps
|
||||
docker logs major-domo-discord-bot-v2 --tail=50
|
||||
```
|
||||
|
||||
Common issues:
|
||||
- Invalid `BOT_TOKEN` - Check .env file
|
||||
- Missing credentials - Check `/data` mount
|
||||
- Database unreachable - Check `DB_URL`
|
||||
|
||||
## Production Deployment
|
||||
|
||||
### Best Practices
|
||||
|
||||
1. **Use specific image tags:**
|
||||
```bash
|
||||
docker tag major-domo/discord-bot-v2:latest major-domo/discord-bot-v2:v2.0.0
|
||||
```
|
||||
|
||||
2. **Enable auto-restart:**
|
||||
```yaml
|
||||
restart: unless-stopped # Already set in compose file
|
||||
```
|
||||
|
||||
3. **Set production environment:**
|
||||
```bash
|
||||
ENVIRONMENT=production
|
||||
LOG_LEVEL=INFO
|
||||
```
|
||||
|
||||
4. **Monitor resource usage:**
|
||||
```bash
|
||||
docker stats major-domo-discord-bot-v2
|
||||
```
|
||||
|
||||
5. **Regular updates:**
|
||||
```bash
|
||||
git pull
|
||||
docker-compose build --no-cache
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### Backup Strategy
|
||||
|
||||
Backup critical data:
|
||||
```bash
|
||||
# Backup logs
|
||||
tar -czf logs-backup-$(date +%Y%m%d).tar.gz logs/
|
||||
|
||||
# Backup configuration
|
||||
cp .env .env.backup
|
||||
|
||||
# Backup credentials
|
||||
cp data/major-domo-service-creds.json data/creds.backup.json
|
||||
```
|
||||
|
||||
## Updates and Maintenance
|
||||
|
||||
### Update Bot Code (Production)
|
||||
|
||||
```bash
|
||||
# 1. Pull latest image from Docker Hub
|
||||
docker-compose pull
|
||||
|
||||
# 2. Restart with new image
|
||||
docker-compose up -d
|
||||
|
||||
# 3. Verify it's running
|
||||
docker-compose logs -f discord-bot
|
||||
```
|
||||
|
||||
### Update Bot Code (Development)
|
||||
|
||||
```bash
|
||||
# 1. Pull latest code
|
||||
git pull
|
||||
|
||||
# 2. Rebuild image
|
||||
docker-compose -f docker-compose.dev.yml build
|
||||
|
||||
# 3. Restart with new image
|
||||
docker-compose -f docker-compose.dev.yml up -d
|
||||
|
||||
# 4. Verify it's running
|
||||
docker-compose -f docker-compose.dev.yml logs -f discord-bot
|
||||
```
|
||||
|
||||
### Update Dependencies
|
||||
|
||||
```bash
|
||||
# 1. Update requirements.txt locally
|
||||
pip install -U discord.py pydantic aiohttp
|
||||
pip freeze > requirements.txt
|
||||
|
||||
# 2. Rebuild image with new dependencies
|
||||
docker-compose build --no-cache
|
||||
|
||||
# 3. Restart
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### Database Migration
|
||||
|
||||
If the database API is updated:
|
||||
|
||||
```bash
|
||||
# 1. Update DB_URL in .env if needed
|
||||
nano .env
|
||||
|
||||
# 2. Restart bot to pick up new configuration
|
||||
docker-compose restart discord-bot
|
||||
|
||||
# 3. Test connectivity
|
||||
docker-compose logs -f discord-bot
|
||||
```
|
||||
|
||||
## File Comparison: Production vs Development
|
||||
|
||||
### `docker-compose.yml` (Production)
|
||||
- Pulls pre-built image from Docker Hub: `manticorum67/major-domo-discordapp:latest`
|
||||
- Environment: `production`
|
||||
- Log level: `INFO` (default)
|
||||
- Resource limits: 512MB RAM, 1 CPU
|
||||
- No source code mounting
|
||||
|
||||
### `docker-compose.dev.yml` (Development)
|
||||
- Builds image locally from Dockerfile
|
||||
- Environment: `development`
|
||||
- Log level: `DEBUG` (default)
|
||||
- Resource limits: 1GB RAM, 2 CPU (more generous)
|
||||
- Optional source code mounting for live updates
|
||||
|
||||
### When to Use Each
|
||||
|
||||
**Use Production (`docker-compose.yml`)**:
|
||||
- Production servers
|
||||
- Staging environments
|
||||
- Any deployment not modifying code
|
||||
- Faster deployment (no build step)
|
||||
|
||||
**Use Development (`docker-compose.dev.yml`)**:
|
||||
- Local development
|
||||
- Testing code changes
|
||||
- Building new features
|
||||
- Debugging issues
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- **Discord.py Documentation**: https://discordpy.readthedocs.io/
|
||||
- **Docker Best Practices**: https://docs.docker.com/develop/dev-best-practices/
|
||||
- **Python 3.13 Release Notes**: https://docs.python.org/3.13/whatsnew/
|
||||
|
||||
## Support
|
||||
|
||||
For issues or questions:
|
||||
1. Check logs: `docker-compose logs discord-bot`
|
||||
2. Review bot documentation in `CLAUDE.md` and command READMEs
|
||||
3. Check health status: `docker inspect major-domo-discord-bot-v2`
|
||||
67
Dockerfile
Normal file
67
Dockerfile
Normal file
@ -0,0 +1,67 @@
|
||||
# ============================================
|
||||
# Stage 1: Builder
|
||||
# ============================================
|
||||
FROM python:3.13-slim AS builder
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /build
|
||||
|
||||
# Install build dependencies
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
gcc \
|
||||
g++ \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy requirements file
|
||||
COPY requirements.txt .
|
||||
|
||||
# Install Python dependencies to a local directory
|
||||
RUN pip install --no-cache-dir --user -r requirements.txt
|
||||
|
||||
# ============================================
|
||||
# Stage 2: Runtime
|
||||
# ============================================
|
||||
FROM python:3.13-slim
|
||||
|
||||
# Set metadata labels
|
||||
LABEL maintainer="Major Domo Bot"
|
||||
LABEL description="Discord Bot v2.0 for Strat-o-Matic Baseball Association"
|
||||
LABEL version="2.0"
|
||||
|
||||
# Set environment variables
|
||||
ENV PYTHONUNBUFFERED=1 \
|
||||
PYTHONDONTWRITEBYTECODE=1 \
|
||||
PATH="/home/botuser/.local/bin:$PATH"
|
||||
|
||||
# Create non-root user
|
||||
RUN groupadd -r botuser && \
|
||||
useradd -r -g botuser -u 1000 -m -s /bin/bash botuser
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Install runtime dependencies only
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy Python dependencies from builder stage
|
||||
COPY --from=builder --chown=botuser:botuser /root/.local /home/botuser/.local
|
||||
|
||||
# Copy application code
|
||||
COPY --chown=botuser:botuser . .
|
||||
|
||||
# Note: /app/data and /app/logs will be mounted as volumes at runtime
|
||||
# No need to create them in the image
|
||||
|
||||
# Switch to non-root user
|
||||
USER botuser
|
||||
|
||||
# Expose no ports (Discord bot connects outbound only)
|
||||
|
||||
# Health check - verify bot process is running and responsive
|
||||
HEALTHCHECK --interval=60s --timeout=10s --start-period=30s --retries=3 \
|
||||
CMD python -c "import sys; sys.exit(0)" || exit 1
|
||||
|
||||
# Set entrypoint
|
||||
CMD ["python", "-u", "bot.py"]
|
||||
97
build-and-push.sh
Executable file
97
build-and-push.sh
Executable file
@ -0,0 +1,97 @@
|
||||
#!/bin/bash
|
||||
# ============================================
|
||||
# Build and Push Docker Image to Docker Hub
|
||||
# ============================================
|
||||
# Usage:
|
||||
# ./build-and-push.sh # Build and push as 'latest'
|
||||
# ./build-and-push.sh 2.0.0 # Build and push as 'latest' and '2.0.0'
|
||||
|
||||
set -e # Exit on error
|
||||
|
||||
# Configuration
|
||||
VERSION="${1:-2.0.0}"
|
||||
DOCKER_REPO="manticorum67/major-domo-discordapp"
|
||||
|
||||
# Color output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
echo -e "${BLUE}======================================${NC}"
|
||||
echo -e "${BLUE}Docker Build and Push${NC}"
|
||||
echo -e "${BLUE}======================================${NC}"
|
||||
echo ""
|
||||
echo -e "${YELLOW}Repository:${NC} ${DOCKER_REPO}"
|
||||
echo -e "${YELLOW}Version:${NC} ${VERSION}"
|
||||
echo ""
|
||||
|
||||
# Check if Docker is running
|
||||
if ! docker info > /dev/null 2>&1; then
|
||||
echo -e "${RED}❌ Error: Docker is not running${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if logged in to Docker Hub
|
||||
if ! docker info 2>/dev/null | grep -q "Username"; then
|
||||
echo -e "${YELLOW}⚠️ Not logged in to Docker Hub${NC}"
|
||||
echo -e "${YELLOW}Please log in:${NC}"
|
||||
docker login
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Build image
|
||||
echo -e "${BLUE}🔨 Building Docker image...${NC}"
|
||||
echo ""
|
||||
|
||||
if [ "$VERSION" = "latest" ]; then
|
||||
# Only tag as latest
|
||||
docker build -t ${DOCKER_REPO}:latest .
|
||||
else
|
||||
# Tag as both latest and version
|
||||
docker build \
|
||||
-t ${DOCKER_REPO}:latest \
|
||||
-t ${DOCKER_REPO}:${VERSION} \
|
||||
.
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}✅ Build complete!${NC}"
|
||||
echo ""
|
||||
|
||||
# Confirm push
|
||||
echo -e "${YELLOW}Ready to push to Docker Hub${NC}"
|
||||
read -p "Continue? (y/n) " -n 1 -r
|
||||
echo ""
|
||||
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo -e "${YELLOW}❌ Push cancelled${NC}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Push image
|
||||
echo ""
|
||||
echo -e "${BLUE}📤 Pushing to Docker Hub...${NC}"
|
||||
echo ""
|
||||
|
||||
docker push ${DOCKER_REPO}:latest
|
||||
|
||||
if [ "$VERSION" != "latest" ]; then
|
||||
docker push ${DOCKER_REPO}:${VERSION}
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}✅ Push complete!${NC}"
|
||||
echo ""
|
||||
echo -e "${GREEN}🎉 Image available at:${NC}"
|
||||
echo -e " docker pull ${DOCKER_REPO}:latest"
|
||||
|
||||
if [ "$VERSION" != "latest" ]; then
|
||||
echo -e " docker pull ${DOCKER_REPO}:${VERSION}"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${BLUE}======================================${NC}"
|
||||
echo -e "${GREEN}Done!${NC}"
|
||||
echo -e "${BLUE}======================================${NC}"
|
||||
@ -27,7 +27,7 @@ class BotConfig(BaseSettings):
|
||||
testing: bool = False
|
||||
|
||||
# Google Sheets settings
|
||||
sheets_credentials_path: str = "/data/major-domo-service-creds.json"
|
||||
sheets_credentials_path: str = "/app/data/major-domo-service-creds.json"
|
||||
|
||||
# Optional Redis caching settings
|
||||
redis_url: str = "" # Empty string means no Redis caching
|
||||
|
||||
78
docker-compose.dev.yml
Normal file
78
docker-compose.dev.yml
Normal file
@ -0,0 +1,78 @@
|
||||
version: '3.8'
|
||||
|
||||
# ============================================
|
||||
# Development Configuration
|
||||
# ============================================
|
||||
# This compose file builds the image locally from source
|
||||
#
|
||||
# Usage:
|
||||
# docker-compose -f docker-compose.dev.yml build
|
||||
# docker-compose -f docker-compose.dev.yml up -d
|
||||
|
||||
services:
|
||||
discord-bot:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
image: major-domo/discord-bot-v2:dev
|
||||
container_name: major-domo-discord-bot-v2-dev
|
||||
|
||||
# Restart policy
|
||||
restart: unless-stopped
|
||||
|
||||
# Environment variables from .env file
|
||||
env_file:
|
||||
- .env
|
||||
|
||||
# Development environment overrides
|
||||
environment:
|
||||
- LOG_LEVEL=${LOG_LEVEL:-DEBUG}
|
||||
- ENVIRONMENT=development
|
||||
- TESTING=${TESTING:-false}
|
||||
- REDIS_URL=${REDIS_URL:-}
|
||||
- REDIS_CACHE_TTL=${REDIS_CACHE_TTL:-300}
|
||||
|
||||
# Volume mounts
|
||||
volumes:
|
||||
# Google Sheets credentials (required)
|
||||
- ${SHEETS_CREDENTIALS_HOST_PATH:-./data}:/app/data:ro
|
||||
|
||||
# Logs directory (persistent) - mounted to /app/logs where the application expects it
|
||||
- ${LOGS_HOST_PATH:-./logs}:/app/logs:rw
|
||||
|
||||
# Optional: Mount source code for live development
|
||||
# Uncomment to enable hot-reloading (requires code changes to handle)
|
||||
# - .:/app:ro
|
||||
|
||||
# Network configuration
|
||||
networks:
|
||||
- major-domo-network
|
||||
|
||||
# Health check
|
||||
healthcheck:
|
||||
test: ["CMD", "python", "-c", "import sys; sys.exit(0)"]
|
||||
interval: 60s
|
||||
timeout: 10s
|
||||
start_period: 30s
|
||||
retries: 3
|
||||
|
||||
# Resource limits (development - more generous)
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
cpus: '2.0'
|
||||
memory: 1G
|
||||
reservations:
|
||||
cpus: '0.25'
|
||||
memory: 256M
|
||||
|
||||
# Logging configuration
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
networks:
|
||||
major-domo-network:
|
||||
driver: bridge
|
||||
74
docker-compose.yml
Normal file
74
docker-compose.yml
Normal file
@ -0,0 +1,74 @@
|
||||
version: '3.8'
|
||||
|
||||
# ============================================
|
||||
# Production Configuration
|
||||
# ============================================
|
||||
# This compose file pulls the pre-built image from Docker Hub
|
||||
#
|
||||
# Usage:
|
||||
# docker-compose pull
|
||||
# docker-compose up -d
|
||||
#
|
||||
# Docker Hub Repository: manticorum67/major-domo-discordapp
|
||||
|
||||
services:
|
||||
discord-bot:
|
||||
# Pull image from Docker Hub
|
||||
image: manticorum67/major-domo-discordapp:latest
|
||||
container_name: major-domo-discord-bot-v2
|
||||
|
||||
# Restart policy
|
||||
restart: unless-stopped
|
||||
|
||||
# Environment variables from .env file
|
||||
env_file:
|
||||
- .env
|
||||
|
||||
# Production environment configuration
|
||||
environment:
|
||||
- LOG_LEVEL=${LOG_LEVEL:-INFO}
|
||||
- ENVIRONMENT=production
|
||||
- TESTING=${TESTING:-false}
|
||||
- REDIS_URL=${REDIS_URL:-}
|
||||
- REDIS_CACHE_TTL=${REDIS_CACHE_TTL:-300}
|
||||
|
||||
# Volume mounts
|
||||
volumes:
|
||||
# Google Sheets credentials (required)
|
||||
- ${SHEETS_CREDENTIALS_HOST_PATH:-./data}:/app/data:ro
|
||||
|
||||
# Logs directory (persistent) - mounted to /app/logs where the application expects it
|
||||
- ${LOGS_HOST_PATH:-./logs}:/app/logs:rw
|
||||
|
||||
# Network configuration
|
||||
networks:
|
||||
- major-domo-network
|
||||
|
||||
# Health check
|
||||
healthcheck:
|
||||
test: ["CMD", "python", "-c", "import sys; sys.exit(0)"]
|
||||
interval: 60s
|
||||
timeout: 10s
|
||||
start-period: 30s
|
||||
retries: 3
|
||||
|
||||
# Resource limits (production)
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
cpus: '1.0'
|
||||
memory: 512M
|
||||
reservations:
|
||||
cpus: '0.25'
|
||||
memory: 256M
|
||||
|
||||
# Logging configuration
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
networks:
|
||||
major-domo-network:
|
||||
driver: bridge
|
||||
@ -1,20 +1,20 @@
|
||||
# Core Framework
|
||||
discord.py>=2.3.0
|
||||
pydantic>=2.0.0
|
||||
pydantic-settings>=2.0.0
|
||||
aiohttp>=3.8.0
|
||||
discord.py==2.5.2
|
||||
pydantic==2.11.7
|
||||
pydantic-settings==2.10.1
|
||||
aiohttp==3.12.13
|
||||
|
||||
# Utilities
|
||||
python-dotenv>=1.0.0
|
||||
redis>=5.0.0 # For optional API response caching
|
||||
python-dotenv==1.1.1
|
||||
redis>=5.0.0 # For optional API response caching (not currently installed)
|
||||
|
||||
# Development & Testing
|
||||
pytest>=7.0.0
|
||||
pytest-asyncio>=0.21.0
|
||||
pytest-mock>=3.10.0
|
||||
aioresponses>=0.7.4
|
||||
black>=23.0.0
|
||||
ruff>=0.1.0
|
||||
pytest==8.4.1
|
||||
pytest-asyncio==1.0.0
|
||||
pytest-mock>=3.10.0 # Not currently installed
|
||||
aioresponses==0.7.8
|
||||
black>=23.0.0 # Not currently installed
|
||||
ruff>=0.1.0 # Not currently installed
|
||||
|
||||
# Optional Dependencies
|
||||
pygsheets>=4.0.0 # For Google Sheets integration (scorecard submission)
|
||||
pygsheets==2.0.6 # For Google Sheets integration (scorecard submission)
|
||||
Loading…
Reference in New Issue
Block a user