Skip to content

MinIO S3 Storage

MinIO provides S3-compatible object storage for the Signal bot’s file sharing functionality. When users request file links via !files command, the bot generates presigned URLs that provide direct, time-limited access to files.

Note: RustFS was briefly evaluated (Dec 2025) for its lower memory footprint (~74 MiB vs 16 GiB), but had issues with presigned URL signature validation. MinIO remains the recommended solution.

ComponentValue
ServiceMinIO (S3-compatible)
Public URLhttps://s3.irregular.chat
Console URLhttps://s3-console.irregular.chat
Bucketirregularchat
Link Expiration24 hours
  1. User searches for files with !files <query>
  2. Bot returns search results from local file index
  3. User replies with a file number to get download link
  4. Bot generates a MinIO presigned URL:
    • Constructs S3 path from category + filename
    • Signs URL with AWS4-HMAC-SHA256 algorithm
    • Sets 24-hour expiration
    • Uses public URL for signature so it matches external access
  5. User receives direct download link
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Signal User │─────▶│ Signal Bot │─────▶│ MinIO Server │
│ !files drone │ │ minio-client.ts │ │ irregularchat │
└─────────────────┘ └──────────────────┘ │ bucket │
│ │ └─────────────────┘
│ │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ Presigned URL │ │
│ │ https://s3... │◀─────────────┘
│ │ ?X-Amz-... │
│ └──────────────────┘
│ │
◀─────────────────────────┘
Download link (15 min expiry)

The signal-bot container connects to MinIO via the minio-network:

services:
signal-bot:
networks:
- signal-bot-network
- minio-network # For MinIO access
networks:
minio-network:
external: true

Located at /home/minio-irregularchat/docker-compose.yml:

version: '3.8'
services:
minio:
image: minio/minio:latest
container_name: minio-irregularchat
restart: unless-stopped
ports:
- "127.0.0.1:9002:9000" # API (internal only)
- "127.0.0.1:9003:9001" # Console (internal only)
environment:
MINIO_ROOT_USER_FILE: /run/secrets/access_key
MINIO_ROOT_PASSWORD_FILE: /run/secrets/secret_key
MINIO_SERVER_URL: https://s3.irregular.chat
MINIO_BROWSER_REDIRECT_URL: https://s3-console.irregular.chat
volumes:
- /datadrive/minio-data:/data:rw
- ./secrets/access_key.txt:/run/secrets/access_key:ro
- ./secrets/secret_key.txt:/run/secrets/secret_key:ro
command: server /data --console-address ":9001"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
interval: 30s
timeout: 10s
retries: 3
networks:
- minio-network
networks:
minio-network:
name: minio-network

Add these to the signal-bot .env file:

Terminal window
# MinIO S3 Configuration
MINIO_ENDPOINT=http://minio-irregularchat:9000
MINIO_ACCESS_KEY=<your-access-key>
MINIO_SECRET_KEY=<your-secret-key>
MINIO_BUCKET=irregularchat
MINIO_REGION=us-east-1
MINIO_PUBLIC_URL=https://s3.irregular.chat
VariableDescription
MINIO_ENDPOINTInternal Docker network endpoint (used by bot)
MINIO_ACCESS_KEYS3-compatible access key
MINIO_SECRET_KEYS3-compatible secret key
MINIO_BUCKETBucket containing files
MINIO_REGIONAWS region (required for S3 signing)
MINIO_PUBLIC_URLPublic URL for presigned links

MinIO presigned URLs can be very long (~300+ characters) which can cause issues in Signal messages:

  • URLs get truncated by “Read More” in long messages
  • URLs may get cut off when copying
  • Long URLs are hard to share manually

The bot optionally integrates with Shlink (self-hosted URL shortener) to create short, memorable URLs:

ComponentValue
Long URLhttps://s3.irregular.chat/irregularchat/...?X-Amz-... (~300 chars)
Short URLhttps://irregular.chat/abc12 (~27 chars)
ExpirationSame as MinIO presigned URL (15 min)

Add these to the signal-bot .env file:

Terminal window
# Shlink URL Shortener (optional)
SHLINK_API_URL=http://shlink-api:8080
SHLINK_API_KEY=<your-shlink-api-key>
SHLINK_PUBLIC_URL=https://irregular.chat
VariableDescription
SHLINK_API_URLInternal Shlink API endpoint (Docker network)
SHLINK_API_KEYShlink API key for authentication
SHLINK_PUBLIC_URLPublic domain for short URLs
  1. Bot generates MinIO presigned URL (long)
  2. If Shlink is configured, bot calls Shlink API to create short URL
  3. Short URL has same 15-minute expiration as presigned URL
  4. User receives short URL: https://irregular.chat/xyz12
  5. Clicking short URL redirects to MinIO presigned URL
  6. After 15 minutes, both URLs expire

If Shlink is not configured (no SHLINK_API_KEY), the bot falls back to providing the full MinIO presigned URL. All functionality still works, just with longer URLs.

The bot maps local file paths to MinIO S3 keys:

Local PathS3 Key
/app/irregularchat/UnmannedSystems/Documents/drone.pdfUnmannedSystems/Documents/drone.pdf
/app/irregularchat/Research/Reports/analysis.pdfResearch/Reports/analysis.pdf

The minio-client.ts utility strips the base path and constructs the S3 key.

Generated URLs follow AWS S3 presigned URL format:

https://s3.irregular.chat/irregularchat/UnmannedSystems/Documents/drone.pdf
?X-Amz-Algorithm=AWS4-HMAC-SHA256
&X-Amz-Credential=...
&X-Amz-Date=20241209T120000Z
&X-Amz-Expires=900
&X-Amz-SignedHeaders=host
&X-Amz-Signature=...
  • Expires: 900 seconds (15 minutes)
  • Algorithm: AWS4-HMAC-SHA256
  • Region: us-east-1

MinIO bucket is mounted at /datadrive/minio-data which mirrors the IrregularChat archive:

/datadrive/
├── IrregularChat/Topics/ # Bot file archive (source)
│ ├── UnmannedSystems/
│ ├── Research/
│ └── ...
└── minio-data/ # MinIO bucket storage
└── irregularchat/
├── UnmannedSystems/
├── Research/
└── ...

Files are synced using rclone:

Terminal window
rclone sync /datadrive/IrregularChat/Topics /datadrive/minio-data/irregularchat

MinIO is exposed via Cloudflare Tunnel (not direct port exposure):

  • API: s3.irregular.chatlocalhost:9002
  • Console: s3-console.irregular.chatlocalhost:9003

This provides:

  • SSL/TLS termination
  • DDoS protection
  • No exposed ports

If MinIO is not configured (missing env vars), the bot falls back to pCloud:

// In file-search.ts
if (isMinioConfigured()) {
// Use MinIO presigned URL
return await getFileLink(s3Key);
} else {
// Fallback to pCloud
return await getPCloudLink(filePath);
}
Terminal window
docker compose exec signal-bot node -e "
const { isMinioConfigured, getMinioConfig } = require('./dist/utils/minio-client.js');
console.log('Configured:', isMinioConfigured());
console.log('Config:', JSON.stringify(getMinioConfig(), null, 2));
"
Terminal window
docker compose exec signal-bot node -e "
const { getFileLink } = require('./dist/utils/minio-client.js');
getFileLink('UnmannedSystems/Documents/test.pdf').then(console.log);
"
Terminal window
curl -f http://localhost:9002/minio/health/live
Terminal window
docker logs minio-irregularchat --tail 50
Terminal window
cd /home/minio-irregularchat
docker compose restart
Terminal window
cd /home/minio-irregularchat
docker compose pull
docker compose up -d

Both MinIO and Signal Bot have restart: unless-stopped, ensuring automatic restart after server reboot.