Deploying Multiple Bot Instances on a Single Server
A system-wide nginx on the host + N isolated Docker stacks located at /home/botN. Each instance has its own Telegram token, domain, and database.
Installing system packages (one time)
sudo apt update
sudo apt install -y nginx certbot python3-certbot-nginx git
curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker $USER && newgrp docker
echo 'fs.inotify.max_user_watches=524288' | sudo tee -a /etc/sysctl.conf
echo 'fs.inotify.max_user_instances=512' | sudo tee -a /etc/sysctl.conf
sudo sysctl -pPort table
| Bot | FPM | Postgres |
|---|---|---|
| bot1 | 9001 | 5433 |
| bot2 | 9002 | 5434 |
| bot3 | 9003 | 5435 |
All ports are bound only to 127.0.0.1.
Deploying the botN instance
Substitute N = 1, 2, 3, …
1. Cloning
sudo git clone <REPO_URL> /home/botN
sudo chown -R www-data:www-data /home/botN
cd /home/botN2. docker-compose.yml
With this approach you need to remove nginx from docker-compose.yml, because we will use the nginx provided by the server.
Example docker-compose.yml configuration:
services:
app:
build: .
user: root
restart: always
depends_on:
- pgdb
env_file:
- .env
working_dir: /var/www/
volumes:
- .:/var/www
ports:
- "127.0.0.1:${APP_FPM_PORT}:9000"
networks:
- pet
dns:
- 8.8.8.8
- 1.1.1.1
pgdb:
image: postgres
tty: true
restart: always
environment:
- POSTGRES_DB=${DB_DATABASE}
- POSTGRES_USER=${DB_USERNAME}
- POSTGRES_PASSWORD=${DB_PASSWORD}
ports:
- "127.0.0.1:${PGDB_PORT}:5432"
volumes:
- pgdata:/var/lib/postgresql
networks:
- pet
queue:
build: .
restart: always
depends_on:
- app
env_file:
- .env
working_dir: /var/www
volumes:
- .:/var/www
command: php artisan queue:work --sleep=3 --tries=3 --timeout=90
networks:
- pet
dns:
- 8.8.8.8
- 1.1.1.1
volumes:
pgdata:
networks:
pet:
driver: bridge3. .env
cp /home/botN/.env.example /home/botN/.env
sudo nano /home/botN/.envIn the .env configuration for each project instance you must specify unique ports for the services.
Minimum set of unique lines:
APP_FPM_PORT=900N
PGDB_PORT=543N+2
APP_URL=https://botN.example.com
DB_HOST=pgdb
DB_PORT=5432
DB_DATABASE=botN
DB_USERNAME=botN
DB_PASSWORD=<openssl rand -hex 24>
TELEGRAM_BOT_TOKEN=...
TELEGRAM_BOT_ID=...
TELEGRAM_GROUP_ID=...
TELEGRAM_SECRET_KEY=<openssl rand -hex 24>Inside Docker all services are still reachable by name (pgdb:5432) — do not confuse these with the host ports.
4. Starting the containers
cd /home/botN
docker compose up -d --build
docker compose psCheck: every port in the output should be 127.0.0.1:900N->9000/tcp and so on.
5. Laravel initialization
cd /home/botN
docker compose exec app composer install --no-dev --optimize-autoloader
docker compose exec app php artisan key:generate
docker compose exec app php artisan migrate --force
docker compose exec app php artisan storage:link
docker compose exec app php artisan config:cache
docker compose exec app php artisan route:cache
docker compose exec app chown -R www-data:www-data /var/www/storage /var/www/bootstrap/cache
docker compose exec app chmod -R 775 /var/www/storage /var/www/bootstrap/cache6. Permissions for the system nginx
ls -ld /home/botNIf the owner is not www-data:
sudo chmod o+x /home/botN
sudo chmod -R o+rX /home/botN/publicCheck: sudo -u www-data ls /home/botN/public/index.php — there should be no "Permission denied".
7. nginx config
sudo nano /etc/nginx/sites-available/botNInsert (replace botN and 900N with the real values):
server {
listen 80;
server_name botN.example.com www.botN.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name botN.example.com www.botN.example.com;
ssl_certificate /etc/letsencrypt/live/botN.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/botN.example.com/privkey.pem;
client_max_body_size 60M;
root /home/botN/public;
index index.php index.html;
access_log /var/log/nginx/botN.access.log;
error_log /var/log/nginx/botN.error.log;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
include fastcgi_params;
fastcgi_pass 127.0.0.1:900N;
fastcgi_index index.php;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_param SCRIPT_FILENAME /var/www/public$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_param HTTPS on;
fastcgi_param HTTP_X_FORWARDED_PROTO https;
fastcgi_read_timeout 120s;
}
location ~ /\.ht { deny all; }
}Critically important:
fastcgi_param HTTPS on;+HTTP_X_FORWARDED_PROTO https;— without them Laravel ends up in an HTTPS redirect loop.root /home/botN/public;— the path on the host.SCRIPT_FILENAME /var/www/public$fastcgi_script_name;— the path inside the container.fastcgi_pass 127.0.0.1:900N— the port of this instance; do not mix them up.
8. Issuing the SSL certificate
sudo certbot --nginx -d bot3.aibot7.ru -d www.bot3.aibot7.ru9. Activating nginx
sudo ln -sf /etc/nginx/sites-available/botN /etc/nginx/sites-enabled/botN
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginx # or start, if nginx was stopped10. Registering the Telegram webhook
https://api.telegram.org/bot{{TELEGRAM_TOKEN}}/setWebhook?url={{TELEGRAM_WEBHOOK}}&max_connections=45&drop_pending_updates=true&secret_token={{TELEGRAM_SECRET_KEY}}&ip_address={{SERVER_IP}}11. Verification
curl -I https://botN.example.com # 200/302 from Laravel
curl https://api.telegram.org/bot<TOKEN>/getWebhookInfo
sudo tail /var/log/nginx/botN.error.log
docker compose -f /home/botN/docker-compose.yml logs --tail=20 app queueDuplicating for additional instances
Repeat the "Deploying the botN instance" section, changing:
- The
/home/botNfolder - All
900N/543Nvalues according to the port table - The domain, Telegram tokens, and database passwords
server_name,root,fastcgi_pass, and the certificate path in the nginx config
Possible errors
Container "/botN_pgdb" is already in use
An old container with a container_name. Remove container_name: from the compose file and delete the old container:
docker rm -f $(docker ps -aq --filter "name=botN")bind() to 0.0.0.0:80 failed (Address already in use)
Port 80 is occupied by another nginx (often a containerized one). Find and kill it:
sudo ss -tlnp | grep ':80\s'
docker stop $(docker ps -q --filter "publish=80")services.volumes additional properties 'pgdata' not allowed
In docker-compose.yml the volumes: and networks: blocks ended up inside services:. Move them to the top-level (zero) indentation.
0.0.0.0:5432->5432/tcp (database exposed to the outside)
In the compose file - ${PGDB_PORT} is missing the IP, or the .env uses the old format PGDB_PORT=5432:5432. Fix:
- compose:
"127.0.0.1:${PGDB_PORT}:5432" .env:PGDB_PORT=5433(just a number)
Welcome to nginx! instead of Laravel
The default site is not disabled, or there is no symlink for botN:
sudo ln -sf /etc/nginx/sites-available/botN /etc/nginx/sites-enabled/botN
sudo rm -f /etc/nginx/sites-enabled/default
sudo systemctl reload nginx502 Bad Gateway
PHP-FPM is unreachable:
sudo ss -tlnp | grep 900N
docker compose ps
sudo tail /var/log/nginx/botN.error.log403 Forbidden
nginx cannot see /home/botN/public/:
sudo chmod o+x /home/botN
sudo chmod -R o+rX /home/botN/publicFile not found from FPM
The SCRIPT_FILENAME is wrong. It should be /var/www/public$fastcgi_script_name (the path inside the container).
ERR_TOO_MANY_REDIRECTS / redirect loop
The listen 443 block is missing fastcgi_param HTTPS on; + fastcgi_param HTTP_X_FORWARDED_PROTO https;. Laravel does not realize the request arrived over HTTPS and redirects to HTTPS again. Add these two parameters and run sudo systemctl reload nginx.
conflicting server name "X" on 0.0.0.0:443, ignored
Two configs declare the same server_name:
sudo grep -rn 'server_name' /etc/nginx/sites-enabled/Remove the duplicate.
Could not automatically find a matching server block
Certbot could not find a block with the required server_name. Do not use --nginx; issue the certificate via certonly --webroot or certonly --standalone, then add the certificate to the config manually (see sections 7–8).
certbot offers Expand/Cancel
The domain already has a certificate without www. Answer E — Let's Encrypt will expand the certificate to cover both domains.
inotify watch limit reached
Not critical, but can be fixed:
echo 'fs.inotify.max_user_watches=524288' | sudo tee -a /etc/sysctl.conf
echo 'fs.inotify.max_user_instances=512' | sudo tee -a /etc/sysctl.conf
sudo sysctl -p