A self-hosted tool to monitor iOS App Store apps for new releases and automatically notify you via multiple platforms (Discord, Slack, Telegram, Microsoft Teams, Email, or custom webhooks). Features a web interface for easy management of multiple apps.
- 🍎 Monitor multiple iOS App Store apps simultaneously
- 🔔 Automatic notifications when new versions are detected (Discord, Slack, Telegram, Teams, Email, or custom webhooks)
- 📝 Formats release notes for better readability across platforms
- 🖥️ Web-based interface for configuration and management
- 🔄 Configurable check intervals per app
- ⚡ Manual check and post buttons for testing
- 🚫 Duplicate prevention - tracks last posted version
- 🔧 Generic settings for reusable webhook configurations (Telegram bot token, SMTP settings)
If you don't have Docker installed, download it from docker.com.
Create a file named docker-compose.yml in a folder on your computer. If you cloned this repository, you can copy the included sample:
cp docker-compose.example.yml docker-compose.ymlOr copy and paste this content (it matches docker-compose.example.yml and the repo’s docker-compose.yml):
services:
app-watch:
image: rajnishdock/app-watch:latest
container_name: app-watch
restart: unless-stopped
ports:
- "8192:8192"
volumes:
- ./data:/data
healthcheck:
test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8192/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 40sNote: The ./data folder will be created automatically to store your app settings and version tracking data.
Custom data directory (e.g. /docker-data/...): Docker Compose automatically merges docker-compose.override.yml if it sits next to docker-compose.yml. Keep the repo sample on ./data, and add an override only on servers where you want a fixed path:
# docker-compose.override.yml (do not commit; listed in .gitignore)
services:
app-watch:
volumes:
- /docker-data/app-watch/data:/dataThen run docker compose up -d as usual; the override replaces the volumes entry for that service.
Open a terminal (Command Prompt on Windows, Terminal on Mac/Linux) in the folder where you created docker-compose.yml and run:
docker compose up -dWait a few seconds for it to start, then open your web browser and go to:
Before adding apps, you need to configure at least one notification destination. The application supports multiple platforms:
Discord:
- Open your Discord server
- Go to Server Settings → Integrations → Webhooks
- Click New Webhook or Create Webhook
- Choose the channel where you want notifications
- Name it (e.g., "App Releases")
- Click Copy Webhook URL and save it
Slack:
- Go to your Slack workspace settings
- Navigate to Apps → Incoming Webhooks
- Click Add to Slack or Create Webhook
- Choose the channel and click Add Incoming Webhooks Integration
- Copy the webhook URL (starts with
https://hooks.slack.com/)
Telegram:
- Message @BotFather on Telegram
- Send
/newbotand follow instructions to create a bot - Copy the bot token (format:
123456789:ABCdefGHIjklMNOpqrsTUVwxyz) - Message @userinfobot to get your chat ID
- You can set the bot token in Settings for all apps, or per app
Microsoft Teams:
- Go to your Teams channel
- Click ⋯ (More options) → Connectors
- Search for "Incoming Webhook" and click Configure
- Name it and click Create
- Copy the webhook URL
Email (SMTP):
- Configure SMTP settings in Settings page (host, port, username, password)
- Or configure per app when adding notification destination
- Common providers: Gmail (smtp.gmail.com:587), Outlook (smtp-mail.outlook.com:587)
Generic Webhook:
- Any HTTP/HTTPS endpoint that accepts POST requests
- Optionally customize the JSON payload template
- Go to apps.apple.com in your browser
- Search for the app you want to monitor
- Open the app's page
- Look at the URL - it will look like:
https://apps.apple.com/app/id123456789 - Copy the number after
/id(that's your App Store ID)
- In the web interface (http://localhost:8192), click "Add App"
- Fill in:
- App Name: Any name you want (e.g., "My App")
- App Store ID: The number you found in Step 5
- App Store Country: 2-letter store code (default
us, e.g.,gb,in,jp) - Notification Destination: Select your preferred platform (Discord, Slack, Telegram, Teams, Email, or Generic)
- Configure the destination: Enter the required information based on your selected platform:
- Discord/Slack/Teams/Generic: Paste the webhook URL
- Telegram: Enter bot token (or use from Settings) and chat ID
- Email: Enter recipient email and SMTP settings (or use from Settings)
- Check Interval (optional): Leave empty for default (12 hours), or use
6h,1d, etc.
- Click "Save"
Note: You can add multiple notification destinations per app. Just add another destination in the same form.
- Click "Check Now" to see if it finds the current version
- Click "Post Now" to send a test message to all configured notification destinations
- If everything works, the app will check for updates automatically
You can add more apps by clicking "Add App" again. Each app can use different notification destinations and channels.
Web interface: App Watch does not provide login, passwords, or other built-in access control for the UI. Anyone who can open the server URL in a browser can use the application (manage apps, change settings, send notifications, and so on).
If you expose the app beyond your own machine (LAN, VPS, port forwarding, cloud host, etc.), you are responsible for restricting who can reach it. Common approaches:
- Reverse proxy with TLS: Place Caddy, nginx, Traefik, or another reverse proxy in front of the container, terminate HTTPS at the proxy, and apply any access rules or authentication the proxy supports. Forward the whole site to the app’s HTTP port (not only
/) so browser reloads on client routes such as/schedulerreach the backend; if the proxy serves static files itself, use an SPA fallback (try_filestoindex.html) for unknown paths. - Private network access: Use Tailscale, WireGuard, another VPN, or SSH port forwarding so the UI is only reachable from trusted devices, not from the public internet.
- Firewall and binding: Restrict inbound traffic with host or cloud firewall rules so only trusted networks or IPs can reach the app port. You can also bind the published port to localhost only (for example
127.0.0.1:8192:8192in Docker Compose) and access the UI via SSH tunnel or VPN.
API key (Settings → Security): The UI shows an API key for scripts and integrations (for example X-Api-Key or Authorization: Bearer). The application does not currently require this key to call the REST API—if someone can reach your instance over the network, they can use the API without it—so network-level protection (above) is what actually restricts access. You should still treat the API key as a secret: do not commit it to repositories, paste it into public issues, or share it unnecessarily, and use Regenerate if it may have leaked, so automation and any future hardening remain trustworthy.
The application periodically checks the App Store API for new versions of your configured apps. When a new version is detected, it automatically formats the release notes and sends notifications to all configured destinations (Discord, Slack, Telegram, Teams, Email, or custom webhooks).
- Default check interval: Every 12 hours (configurable per app)
- Custom intervals: Set different check frequencies per app (e.g.,
6hfor 6 hours,1dfor daily) - Manual checks: Use the "Check Now" button to trigger an immediate check
- Multiple destinations: Configure multiple notification channels per app (e.g., Discord + Email)
- Duplicate prevention: Tracks the last posted version to avoid sending the same update multiple times
- Version tracking: Stores version history locally in the data directory
- Generic settings: Set reusable configurations (Telegram bot token, SMTP settings) in Settings page
The application automatically formats release notes from the App Store for better readability across all platforms. It detects structured sections and formats them appropriately for each notification type (Discord, Slack, Telegram, Teams, Email, etc.).
Original App Store release notes:
New:
- Dark mode support
- New dashboard design
Improvements:
- Faster app startup
- Better error handling
Fixed:
- Crash on login
- Memory leak issue
Formatted output (Discord/other platforms):
# v2.3.1
## New
- Dark mode support
- New dashboard design
## Improvements
- Faster app startup
- Better error handling
## Fixed
- Crash on login
- Memory leak issue
The formatter automatically detects and formats these section headers appropriately for each platform:
- New (or "new:")
- Added (or "added:")
- Improvements (or "improvements:", "improved:")
- Fixed (or "fixed:", "fixes:", "bugs:", "bug:")
- Changes (or "changes:", "change:")
If your release notes don't have these section headers, they'll be formatted as a simple bullet list with the version number.
Original App Store release notes:
This release includes bug fixes and performance improvements.
We've also added support for iOS 17.
Formatted output (Discord/other platforms):
# v2.3.1
- This release includes bug fixes and performance improvements.
- We've also added support for iOS 17.
The formatting behavior can be customized by modifying the formatter configuration in the source code. The formatter recognizes common section headers and can be extended to support additional patterns if needed.
Start the application:
docker compose up -dStop the application:
docker compose downRestart the application:
docker compose restartView logs:
docker logs app-watchView logs in real-time:
docker logs -f app-watchUsing Docker Run (without Docker Compose):
docker run -d \
--name app-watch \
--restart unless-stopped \
-p 8192:8192 \
-v $(pwd)/data:/data \
rajnishdock/app-watch:latestBuilding from Source:
If you want to build from source or make modifications:
# Clone the repository
git clone https://github.com/YOUR_USERNAME/app-watch.git
cd app-watch
# Build frontend
cd frontend
npm install
npm run build
cd ..
# Build and run with Docker Compose
docker compose build
docker compose up -dWith the default Compose sample (docker-compose.yml / docker-compose.example.yml), configurations and version tracking live in a data folder next to your compose file:
data/apps.json- App configurations (names, IDs, notification destinations, intervals)data/settings.json- Global settings (default interval, Telegram bot token, SMTP settings)data/apps/<APP_ID>/version.txt- Last posted version for each appdata/apps/<APP_ID>/check.txt- Last check timestamp for each app
Important: If you delete the data folder, you'll lose all your app configurations and version tracking.
The docker-compose.yml file supports many configuration options. Here's a comprehensive example with all possible values:
services:
app-watch:
# Image configuration
image: rajnishdock/app-watch:latest # Docker image to use
container_name: app-watch # Custom container name (optional)
# Restart policy
restart: unless-stopped # Options: no, always, on-failure, unless-stopped
# Port mapping (host:container)
ports:
- "8192:8192" # Format: "HOST_PORT:CONTAINER_PORT"
# Alternative formats:
# - "8192:8192/tcp" # Specify protocol
# - "127.0.0.1:8192:8192" # Bind to specific host IP
# Volume mounts (default: ./data — see docker-compose.example.yml)
volumes:
- ./data:/data # Local path next to compose file
# Other hosts: use docker-compose.override.yml with an absolute path or named volume
# - /docker-data/app-watch/data:/data
# - app-watch-data:/data # Named volume (requires volumes: section)
# Environment variables
environment:
- CHECK_INTERVAL=12h # Default check interval (12h, 6h, 1d, 30m, etc.)
- PORT=8192 # Server port (default: 8192)
- GUNICORN_WORKERS=2 # Gunicorn worker processes (container default: 2)
- GUNICORN_THREADS=4 # Threads per worker (container default: 4)
- GUNICORN_TIMEOUT=60 # Worker timeout in seconds (container default: 60)
- TZ=America/New_York # Timezone (optional, e.g., UTC, Europe/London)
- APP_VERSION=1.0.0 # App version override (optional)
# Alternative: use env_file
# env_file:
# - .env # Load from .env file
# - .env.local # Multiple files supported
# Networks (optional)
networks:
- app-watch-network # Custom network name
# Or use default network
# Health check (optional) — uses lightweight /health (see Dockerfile HEALTHCHECK)
healthcheck:
test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8192/health"]
interval: 30s # Check every 30 seconds
timeout: 5s # Fail if the probe runs longer than this
retries: 3 # Retry 3 times before marking unhealthy
start_period: 40s # Grace period on startup
# Resource limits (optional)
deploy:
resources:
limits:
cpus: '1.0' # CPU limit (1.0 = 1 CPU core)
memory: 512M # Memory limit
reservations:
cpus: '0.5' # CPU reservation
memory: 256M # Memory reservation
# Logging configuration (optional)
logging:
driver: "json-file" # Options: json-file, syslog, journald, gelf, fluentd, awslogs, splunk, etwlogs, none
options:
max-size: "10m" # Max log file size
max-file: "3" # Number of log files to keep
# Labels (optional)
labels:
- "com.example.description=App Watch"
- "com.example.version=1.0"
# User and permissions (optional)
# user: "1000:1000" # Run as specific user:group (UID:GID)
# Working directory (optional)
# working_dir: /app # Override working directory
# Command override (optional)
# command: ["python", "-m", "backend.app", "--custom-arg"]
# Entrypoint override (optional)
# entrypoint: ["/custom-entrypoint.sh"]
# Security options (optional)
# security_opt:
# - no-new-privileges:true
# Capabilities (optional)
# cap_add:
# - NET_ADMIN
# cap_drop:
# - ALL
# Shared memory size (optional)
# shm_size: '64mb'
# Dependencies (optional)
# depends_on:
# - database
# - redis
# Named volumes (if using named volumes)
# volumes:
# app-watch-data:
# driver: local
# # Optional volume options:
# # driver_opts:
# # type: none
# # o: bind
# # device: /path/to/data
# Networks (if using custom networks)
# networks:
# app-watch-network:
# driver: bridge
# # Optional network options:
# # ipam:
# # config:
# # - subnet: 172.20.0.0/16| Variable | Description | Default | Format/Examples |
|---|---|---|---|
CHECK_INTERVAL |
Default check interval for apps without custom intervals | 12h |
30m, 6h, 12h, 1d, 7d |
PORT |
Server port number | 8192 |
Any valid port number (e.g., 3000, 8080) |
GUNICORN_WORKERS |
Gunicorn worker process count | 2 |
Integer (e.g., 2, 3, 4) |
GUNICORN_THREADS |
Threads per Gunicorn worker (gthread) |
4 |
Integer (e.g., 2, 4, 8) |
GUNICORN_TIMEOUT |
Gunicorn worker request timeout (seconds) | 60 |
Integer seconds (e.g., 30, 60, 120) |
TZ |
Timezone for timestamps and logging | System timezone | UTC, America/New_York, Europe/London, Asia/Tokyo |
APP_VERSION or VERSION |
Application version override | Auto-detected | Version string (e.g., 1.0.0) |
no: Do not automatically restart the container (default)always: Always restart the container if it stopson-failure: Restart the container if it exits due to an errorunless-stopped: Always restart the container unless it is explicitly stopped
- Bind mount:
./data:/data(default indocker-compose.example.yml) or an absolute path — for a different path on one host, preferdocker-compose.override.ymlover editing the shared compose file - Named volume:
app-watch-data:/data- Uses Docker-managed volume (requiresvolumes:section) - Read-only mount:
./data:/data:ro- Mount as read-only
"8192:8192"- Map host port 8192 to container port 8192"127.0.0.1:8192:8192"- Bind to specific host IP only"8192:8192/tcp"- Specify protocol (tcp/udp)"8192:8192/udp"- UDP protocol
The image defines a HEALTHCHECK that probes GET /health on 127.0.0.1 using the container’s PORT (default 8192). That endpoint returns only {"status":"ok"} and does not run the scheduler or touch disk, so it is safe for frequent liveness probes.
Use GET /api/status when you want richer JSON (version, scheduler thread, job count); it may restart the scheduler if the worker thread died, so it is better for monitoring than for high-frequency Docker health checks.
If you override PORT, override the health check URL in Compose/Kubernetes to use the same port.
Kubernetes example (liveness):
livenessProbe:
httpGet:
path: /health
port: 8192
initialDelaySeconds: 40
periodSeconds: 30
timeoutSeconds: 5
failureThreshold: 3You can customize Compose healthcheck fields as needed:
interval: Time between health checkstimeout: Maximum time to wait for a responseretries: Number of consecutive failures before marking unhealthystart_period: Grace period on startup before health checks count
Use the deploy.resources section to limit container resources:
limits: Maximum resources the container can usereservations: Guaranteed minimum resources
Note: The deploy section is primarily for Docker Swarm, but some options work with docker compose. For standalone Docker Compose, you can also use deprecated but still functional options:
mem_limit: 512m
cpus: 1.0Common logging driver options:
json-file: Default, logs to JSON files (supportsmax-sizeandmax-fileoptions)syslog: Send logs to syslogjournald: Send logs to systemd journal (Linux only)none: Disable logging
Each app can be configured individually through the web interface:
- App Name: Display name for easy identification
- App Store ID: Unique identifier from the App Store URL
- Notification Destinations: One or more notification channels (Discord, Slack, Telegram, Teams, Email, or Generic webhook)
- Each destination can be configured with its specific settings
- You can add multiple destinations of the same or different types
- Check Interval: Override the default interval (e.g.,
6h,30m,1d) - Enabled: Toggle to enable/disable monitoring for specific apps
Configure reusable settings in the Settings page:
- Default Check Interval: Default interval for all apps (unless overridden)
- Monitoring Enabled by Default: Whether new apps start enabled
- Auto-Post on Update: Automatically send notifications when updates are detected
- Telegram Bot Token: Default bot token for all Telegram notifications (can be overridden per app)
- SMTP Settings: Default email server settings (host, port, username, password, from address, TLS)
- These can be used for all email notifications or overridden per app
Port already in use:
- Check if port 8192 is already in use by another application
- On Linux/Mac:
lsof -i :8192ornetstat -an | grep 8192 - On Windows:
netstat -ano | findstr :8192 - Change the port in
docker-compose.ymlif needed
Permission issues:
- Ensure Docker has permission to access the data directory
- On Linux/Mac:
chmod 755 ./data(if the folder exists) - The
./datafolder will be created automatically with proper permissions
Docker not running:
- Verify Docker Desktop (or Docker daemon) is running
- Check Docker status:
docker ps
Discord/Slack/Teams/Generic Webhook issues:
- Verify the webhook URL is correct:
- Discord: Must start with
https://discord.com/api/webhooks/ - Slack: Must start with
https://hooks.slack.com/ - Teams: Must be a valid HTTPS URL
- Generic: Must start with
http://orhttps://
- Discord: Must start with
- Ensure you copied the entire URL without any extra spaces or characters
- Test the webhook manually using curl:
curl -X POST -H "Content-Type: application/json" \ -d '{"content":"Test message"}' \ YOUR_WEBHOOK_URL
Webhook deleted or invalid:
- If you deleted the webhook, create a new one and update the app configuration
- Check that the webhook has permission to post in the selected channel
- Verify the webhook is still active in the platform's settings
Telegram issues:
- Verify the bot token is correct (format:
123456789:ABCdef...) - Check that the chat ID is correct (get it from @userinfobot)
- Ensure the bot token is set either in Settings or per app
- Make sure you've started a conversation with your bot first
Email (SMTP) issues:
- Verify SMTP settings are correct (host, port, username, password)
- For Gmail, use an App Password instead of your regular password
- Check that SMTP settings are set either in Settings or per app
- Verify the recipient email address is correct
- Test SMTP connection manually if needed
No notifications received:
- Use the "Post Now" button to test manually
- Check the container logs for errors:
docker logs app-watch - Verify the app is enabled in the web interface
- Check that at least one notification destination is properly configured
App Store ID incorrect:
- Double-check the App Store ID contains only numbers
- Verify the ID by visiting:
https://apps.apple.com/app/id<YOUR_ID> - The ID should be in the URL format:
apps.apple.com/app/id123456789
Check interval too long:
- Reduce the check interval to test more frequently (e.g.,
30mfor testing) - Use the "Check Now" button to trigger an immediate check
- Check the "Last Check" timestamp in the web interface
App Store API issues:
- The App Store API may be slow or temporarily unavailable
- Wait a few minutes and try the "Check Now" button again
- Check container logs for API errors:
docker logs app-watch
"Invalid Discord webhook URL"
- The URL must start with
https://discord.com/api/webhooks/ - Ensure there are no spaces or extra characters
- If using an old webhook URL with
discordapp.com, update it todiscord.com
"Invalid Slack webhook URL"
- The URL must start with
https://hooks.slack.com/ - Ensure you copied the complete webhook URL from Slack
"Telegram bot token is required"
- Set the bot token in Settings page, or provide it when configuring the app
- Get the token from @BotFather on Telegram
"Telegram chat ID is required"
- Get your chat ID from @userinfobot on Telegram
- Or extract it from Telegram message updates
"SMTP host is required"
- Set SMTP host in Settings page, or provide it when configuring email destination
- Common values:
smtp.gmail.com,smtp-mail.outlook.com
"Invalid webhook URL"
- For generic webhooks, ensure the URL starts with
http://orhttps:// - Verify the endpoint accepts POST requests with JSON payload
"App Store ID must be a number"
- Only use the numeric ID from the App Store URL
- Example: For
https://apps.apple.com/app/id123456789, use123456789 - Remove any non-numeric characters
"Invalid interval format"
- Use the format:
<number><unit>where unit ism(minutes),h(hours), ord(days) - Examples:
30m,6h,12h,1d,7d - Leave empty to use the default interval (12 hours)
The application provides a REST API for programmatic access. Integrations can send the API key from Settings → Security as X-Api-Key: <your-key> or Authorization: Bearer <your-key> where supported. The server does not enforce authentication on these endpoints today—whoever can reach the service can call the API—so rely on network exposure controls for real protection, and still keep the API key private.
GET /health- Lightweight liveness (JSON{"status":"ok"}); intended for Docker/Kubernetes probesGET /api/apps- List all configured appsPOST /api/apps- Create a new app configurationPUT /api/apps/:id- Update an existing appDELETE /api/apps/:id- Delete an app configurationPOST /api/apps/:id/check- Manually trigger a check for updatesPOST /api/apps/:id/post- Manually post current version to all configured notification destinationsGET /api/settings- Get application settingsPUT /api/settings- Update application settingsGET /api/status- Status JSON (version, scheduler, jobs); may restart the scheduler if the worker thread died
- Backend: Python 3.11 with Flask
- Frontend: React 18
- WSGI server: Gunicorn (
gthreadworker class) in the container - Scheduling: Automatic checks using the
schedulelibrary - Storage: JSON-based file storage for app configurations
- Container: Docker with multi-stage builds
App Watch includes a small pytest suite that covers storage behavior and core API endpoints.
python3 -m pip install -r requirements-dev.txt
python3 -m pytestContributions are welcome! If you'd like to contribute:
- Fork the repository
- Create a feature branch
- Make your changes
- Submit a pull request
MIT License - feel free to use this for personal or commercial projects.