// DevOps
How deployment and admin panels are set up: phpMyAdmin and RabbitMQ behind nginx
Published on 2026-05-29
How deployment and admin panels are arranged: phpMyAdmin and RabbitMQ behind nginx
In this note we’ll break down a practical deployment scheme of an application using GitHub Actions and Docker Compose, as well as connecting admin panels phpMyAdmin and RabbitMQ through a single nginx gateway.
The main idea is simple: only nginx faces the outside, and all internal services — API, frontend, database, phpMyAdmin, RabbitMQ and workers — remain inside the internal Docker network. Deployment is fully automated: after a push to main CI builds the environment, synchronizes code to the server, rebuilds containers, runs migrations and scales workers.
Public domains, server names and paths in the article are anonymized. Examples use example.com, api.example.com, pma.example.com, rmq.example.com and the example path /srv/project.
Overall diagram
GitHub (push → main)
│
▼
GitHub Actions
┌─────────────────────────────┐
│ 1. Build .env │
│ 2. Generate .htpasswd │
│ 3. rsync → server │
│ 4. docker compose build │
│ 5. docker compose up -d │
│ 6. Migrations │
│ 7. nginx -s reload │
│ 8. Scale workers │
└─────────────────────────────┘
│
▼
Application server
┌──────────────────────────────────────────┐
│ gateway (nginx) ← the only entry point │
│ ├── example.com → frontend │
│ ├── api.example.com → api-php-fpm │
│ ├── pma.example.com → phpmyadmin │
│ └── rmq.example.com → rabbitmq:15672 │
└──────────────────────────────────────────┘
Here gateway is an nginx container that accepts HTTP/HTTPS traffic and proxies requests into the internal Docker network. Only ports 80 and 443 are open on the server. phpMyAdmin and RabbitMQ do not publish their ports directly to the outside.
Part 1. What happens on push to main
Step 1. Building .env
The .env file is not stored in Git. It is created anew on every deploy right in CI. This reduces the risk of accidentally publishing secrets in the repository and simplifies environment management.
All secrets live in GitHub Actions under:
Repository → Settings → Secrets and variables → Actions
The separation logic is:
vars.*— non-secret settings: service names, domains, run modes, public parameters;secrets.*— passwords, tokens, private keys and any values that should not be shown in logs or stored in code.
Example CI step:
# deploy.yml
- name: Create .env
env:
MYSQL_USER: ${{ vars.MYSQL_USER }}
MYSQL_DATABASE: ${{ vars.MYSQL_DATABASE }}
MYSQL_PASSWORD: ${{ secrets.MYSQL_PASSWORD }}
RABBITMQ_USER: ${{ vars.RABBITMQ_USER }}
RABBITMQ_PASSWORD: ${{ secrets.RABBITMQ_PASSWORD }}
RABBITMQ_VHOST: ${{ vars.RABBITMQ_VHOST }}
run: |
echo "DB_URL=mysql://${MYSQL_USER}:${MYSQL_PASSWORD}@db:3306/${MYSQL_DATABASE}" >> .env
echo "MESSENGER_TRANSPORT_DSN=amqp://${RABBITMQ_USER}:${RABBITMQ_PASSWORD}@rabbitmq:5672/${RABBITMQ_VHOST}" >> .env
Important point: GitHub masks values from secrets, but you shouldn’t intentionally print them to the log. Even if secrets are hidden, it’s better to design the pipeline so that passwords and tokens don’t go to stdout at all.
Also watch out for special characters in passwords. If a password contains characters significant for URLs, it needs to be properly encoded, otherwise the DSN can become invalid. This is especially relevant for connection strings like:
mysql://user:password@db:3306/database
amqp://user:password@rabbitmq:5672/vhost
Step 2. Generating .htpasswd for panels
At this stage, basic-auth files for nginx are created. They are needed to protect admin panels with an additional browser login and password.
A separate login is used for phpMyAdmin:
# For phpMyAdmin — separate login
mkdir -p gateway/docker/production/nginx/auth
echo "${PMA_AUTH_USER}:$(openssl passwd -apr1 "${PMA_AUTH_PASSWORD}")" \
> gateway/docker/production/nginx/auth/.htpasswd
For RabbitMQ, the same credentials as RabbitMQ itself are used:
# For RabbitMQ — the same credentials as RMQ are used
mkdir -p gateway/docker/production/nginx/auth
echo "${RABBITMQ_USER}:$(openssl passwd -apr1 "${RABBITMQ_PASSWORD}")" \
> gateway/docker/production/nginx/auth/.htpasswd_rmq
The command:
openssl passwd -apr1 "password"
creates a hash in Apache MD5 format, also known as apr1. Nginx can read such hashes from files referenced by the auth_basic_user_file directive.
Files end up in the directory:
gateway/docker/production/nginx/auth/
After that they are synchronized to the server together with the code.
These files should not be stored in Git. The directory with auth files or the files themselves should be added to .gitignore:
gateway/docker/production/nginx/auth/.htpasswd
gateway/docker/production/nginx/auth/.htpasswd_rmq
Otherwise you might accidentally commit password hashes. A hash is not the plain password, but it still should not be published: it can be brute-forced offline.
Step 3. rsync to the server
After preparing .env and .htpasswd, the code is synchronized to the server.
Example command:
rsync -avz --delete \
--exclude='.git' \
--exclude='api/vendor' \
--exclude='frontend/node_modules' \
./ deploy@app-server:/srv/project/
The --delete flag means that files deleted in the CI working copy will be deleted on the server as well. This is useful for a clean sync so the server doesn’t become a storage of old files.
But --delete has an important consequence: anything that should live only on the server must not be placed in the directory that rsync fully overwrites. For example, runtime data, uploads, database volume data and files created by the app are better stored outside the sync directory or mounted via Docker volumes.
In this setup excluded from sync are:
--exclude='.git'
--exclude='api/vendor'
--exclude='frontend/node_modules'
This makes sense: .git is not needed on the production server, PHP and Node.js dependencies are usually installed or built inside images, not copied as local developer directories.
Steps 4–8. Deploying on the server
After synchronization CI connects to the server and runs Docker Compose commands.
First new images are built:
# Build new images. Old containers are still running at this point.
docker compose -f docker-compose-production.yml build
This step doesn’t switch the app by itself. It only builds new image versions. Old containers keep running until up is executed.
Next containers are updated:
# Switch containers
docker compose -f docker-compose-production.yml up -d --remove-orphans
up -d runs containers in the background. If a service configuration or image changed, Compose recreates the corresponding containers. The --remove-orphans flag removes containers from services that were in the compose file before but are now removed from the configuration.
Downtime is usually minimal, but don’t call this scheme a full zero-downtime deployment. If the API container is recreated as a single instance, a short disruption is possible. For true zero-downtime you need additional mechanisms: multiple replicas, healthchecks, proper traffic switching and a careful update strategy.
After starting containers, migrations are run:
# Wait for MySQL and run migrations
docker compose -f docker-compose-production.yml run --rm api-php-cli \
php bin/console doctrine:migrations:migrate -n
A separate CLI container of the application is used here. This is the correct approach: migrations run in the same environment as the app, with the same environment variables and dependencies.
Before reloading nginx it is better to check the configuration:
# Check nginx configuration
docker compose -f docker-compose-production.yml exec gateway nginx -t
# Reload nginx config without fully restarting the container
docker compose -f docker-compose-production.yml exec gateway nginx -s reload
nginx -s reload reloads the configuration without stopping the whole container. But if the configuration is invalid, reload may cause problems. So nginx -t before reload is a simple and useful safeguard.
At the end workers are scaled:
# Scale workers
docker compose -f docker-compose-production.yml up -d \
--scale api-workers=5 \
--scale log-workers=1 \
--no-recreate
The --scale flag sets the number of containers for a given service. For example, here five instances of api-workers and one instance of log-workers are started.
The --no-recreate flag tells Compose not to recreate already existing containers unless necessary. This is convenient for workers: you can change the number of replicas without unnecessary recreation of what’s already running.
Part 2. How phpMyAdmin and RabbitMQ are connected
Network isolation
All containers are inside an internal Docker network, let’s call it backend.
Only ports 80 and 443 are open externally, and both are listened to by gateway — the nginx container.
Internet → 443 → gateway (nginx) → internal Docker network
├── frontend
├── api-php-fpm
├── phpmyadmin:80
└── rabbitmq:15672
phpMyAdmin and RabbitMQ do not have published external ports. They are reachable only via gateway.
This is an important security point. Even if someone knows the internal container name or the standard RabbitMQ port, they cannot reach it directly from the internet.
Nginx config for phpMyAdmin
Example pma.conf file:
server {
listen 443 ssl;
server_name pma.example.com;
# Basic auth — requests a login/password before any request
auth_basic "Admin Area";
auth_basic_user_file /etc/nginx/auth/.htpasswd;
location / {
proxy_pass http://phpmyadmin;
}
}
How it works:
- The browser goes to
https://pma.example.com. - Nginx shows a browser dialog “enter login/password”.
- Nginx checks the entered credentials against the
/etc/nginx/auth/.htpasswdfile. - If credentials don’t match — nginx returns
401 Unauthorized. - If credentials match — nginx proxies the request to the
phpmyadmincontainer on port80.
The line:
proxy_pass http://phpmyadmin;
works thanks to Docker DNS. The name phpmyadmin must match the service name or network alias in Docker Compose, and the nginx container must be in the same Docker network.
If nginx is not in the same network, the container name will not resolve and nginx will give an error like:
host not found in upstream "phpmyadmin"
Nginx config for RabbitMQ
Example rmq.conf file:
server {
listen 443 ssl;
server_name rmq.example.com;
auth_basic "Admin Area";
auth_basic_user_file /etc/nginx/auth/.htpasswd_rmq;
location / {
proxy_pass http://rabbitmq:15672;
}
}
The logic here is the same as for phpMyAdmin, but the upstream is different:
proxy_pass http://rabbitmq:15672;
Port 15672 is the HTTP interface of RabbitMQ Management. It is used for the web UI and the HTTP management API.
In this setup the management variant of the RabbitMQ image is used:
rabbitmq:3.13.2-management
This image has the management plugin enabled, so the RabbitMQ web UI is available inside the Docker network on port 15672.
Why RabbitMQ ends up with two-layer auth
RabbitMQ Management has its own authorization: the user enters RabbitMQ login and password in the RabbitMQ Management UI form.
But before that there is nginx basic auth.
So there are two layers:
Browser
│
▼
nginx basic auth
│
▼
RabbitMQ Management auth
│
▼
RabbitMQ Management UI
In practice the user goes through two logins:
- First the browser basic-auth dialog from nginx.
- Then the RabbitMQ Management login form.
Why this is useful:
- The RabbitMQ Management UI is not exposed to the internet without prior nginx authorization.
- Scanners and random visitors do not see the RabbitMQ form directly.
- Even if the RabbitMQ password is known, an outer basic auth layer must still be passed.
Important: nginx basic auth does not replace RabbitMQ authorization. It is an additional layer in front of the management UI.
Part 3. SSL certificates
Let’s Encrypt SSL certificates cover all public names of the project:
example.com
www.example.com
api.example.com
rmq.example.com
pma.example.com
certbot runs alongside the main containers. It checks certificate expiration on a schedule and renews certificates automatically.
Nginx reads certificates from a shared volume, for example:
/etc/letsencrypt/live/example.com/fullchain.pem
/etc/letsencrypt/live/example.com/privkey.pem
or from another path inside the container if the volume is mounted differently.
The overall scheme is:
certbot → updates certificates → shared volume → nginx reads certificates
After a successful certificate renewal nginx must reload certificates. In a containerized setup this is usually solved by one of:
- a certbot deploy-hook that calls nginx reload;
- a separate maintenance script;
- a reload command inside CI/CD;
- periodic safe nginx reload after checking configuration.
A minimal check for automatic renewal:
certbot renew --dry-run
If certbot runs in a container the command will look like with Docker Compose, for example:
docker compose -f docker-compose-production.yml run --rm certbot renew --dry-run
After updating a certificate it’s useful to check which certificate nginx actually serves:
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
| openssl x509 -noout -dates -issuer -subject
This helps catch a situation where certbot updated the certificate but nginx still serves the old certificate from memory.
Part 4. Emergency manual management
Automatic deployment covers the usual scenario, but you always need a clear emergency path: how to view logs, restart services, run migrations or perform a full redeploy.
Full redeploy
If something is broken you can run a separate workflow manually:
GitHub → Actions → Full Redeploy → Run workflow
Such a workflow may be called redeploy.yml.
Its task is not just to update changed containers, but to fully rebuild the environment:
# Stop containers
docker compose -f docker-compose-production.yml down
# Build images from scratch
docker compose -f docker-compose-production.yml build --no-cache
# Start the environment again
docker compose -f docker-compose-production.yml up -d --remove-orphans
This mode is useful if you suspect a corrupted Docker image layer, conflicting old containers or an inconsistent state after a failed deploy.
But this is a harsher operation than a normal deploy: it may cause greater downtime, so use it as an emergency tool.
View gateway logs
To view nginx-gateway logs:
docker compose -f docker-compose-production.yml logs -f gateway
This command is needed if:
- frontend does not open;
- API returns an error via nginx;
- phpMyAdmin doesn’t work;
- RabbitMQ Management UI doesn’t open;
- there is a suspicion of an upstream error;
- nginx cannot find a container by name;
- new config did not apply.
Typical errors you may see in logs:
host not found in upstream
connect() failed
upstream timed out
SSL_do_handshake() failed
no user/password was provided for basic authentication
user "..." was not found in "/etc/nginx/auth/.htpasswd"
View RabbitMQ logs
For RabbitMQ:
docker compose -f docker-compose-production.yml logs -f rabbitmq
This command helps to check:
- whether RabbitMQ started;
- whether applications connected to the broker;
- whether the necessary vhost was created;
- whether there are authorization errors;
- whether queues are working;
- whether there are disk or memory issues.
Access MySQL manually through the app
For a simple check of DB connectivity you can run an SQL query via the Symfony CLI container:
docker compose -f docker-compose-production.yml run --rm api-php-cli \
php bin/console doctrine:query:sql "SELECT 1"
If the command returns a result, that means:
- the app CLI container starts;
- environment variables are read;
- the DSN to the database is correct;
- MySQL is accessible from the Docker network;
- Doctrine can execute a query.
For diagnostics this is often more convenient than going directly into the MySQL container, because it checks the application’s path to the database.
What is important to check before publishing such a scheme to production
Below is not a new architecture, but a checklist for the described scheme. It helps not to miss small things that usually break deployment at the most inconvenient moment.
1. .env must not get into Git
Check .gitignore:
.env
.env.*
If templates are needed, keep only an example:
.env.example
It should contain variable names without real secrets.
2. Auth files must not get into Git
Check:
gateway/docker/production/nginx/auth/.htpasswd
gateway/docker/production/nginx/auth/.htpasswd_rmq
You can exclude the whole directory:
gateway/docker/production/nginx/auth/
But then make sure CI creates the directory before writing files:
mkdir -p gateway/docker/production/nginx/auth
3. Nginx must be in the same Docker network as phpMyAdmin and RabbitMQ
If nginx config has:
proxy_pass http://phpmyadmin;
proxy_pass http://rabbitmq:15672;
then the gateway container must see phpmyadmin and rabbitmq services via Docker DNS.
You can check this way:
docker compose -f docker-compose-production.yml exec gateway getent hosts phpmyadmin
docker compose -f docker-compose-production.yml exec gateway getent hosts rabbitmq
If names do not resolve, the problem is usually Docker Compose networks: services reside in different networks or nginx is not attached to the required network.
4. Run nginx -t before reload
Safe order:
docker compose -f docker-compose-production.yml exec gateway nginx -t
docker compose -f docker-compose-production.yml exec gateway nginx -s reload
If nginx -t fails, do not reload. Fix the configuration first.
5. Check certbot with dry-run
Command:
certbot renew --dry-run
or containerized variant:
docker compose -f docker-compose-production.yml run --rm certbot renew --dry-run
should succeed after initial setup and after changes in nginx, DNS, firewall or reverse proxy.
6. Prefer not to publish RabbitMQ Management directly
In this scheme the management UI is accessible only through nginx and basic auth. This is better than exposing port 15672 to the outside via Docker ports.
So for production it’s better to avoid this:
ports:
- "15672:15672"
If the port is published externally, RabbitMQ Management UI will be directly accessible, bypassing nginx basic auth.
Conclusion
The scheme is built around a simple idea: all external traffic goes through a single nginx gateway, and internal services remain closed inside the Docker network.
On push to main GitHub Actions:
- Creates
.envfromvarsandsecrets. - Generates
.htpasswdfor phpMyAdmin and RabbitMQ. - Synchronizes code to the server via
rsync. - Builds Docker images.
- Updates containers via Docker Compose.
- Runs migrations.
- Reloads nginx.
- Scales workers.
phpMyAdmin and RabbitMQ are not opened directly to the internet. They are accessible only through nginx:
pma.example.com → nginx basic auth → phpMyAdmin
rmq.example.com → nginx basic auth → RabbitMQ Management auth → RabbitMQ UI
For production this is a reasonable and clear scheme: secrets are not stored in Git, admin panels are protected by an additional auth layer, SSL is renewed automatically, and emergency management remains available via separate commands and a manual workflow.
The main thing is not to forget the technical checks: .gitignore for secrets, nginx -t before reload, certbot renew --dry-run, not publishing 15672 directly to the outside, and correct Docker network connections between gateway, phpmyadmin and rabbitmq.
// Reviews
Related reviews
The collaboration left an extremely positive impression, primarily because of the professionalism and the approach to resolving issues as they arose.
The experience of working together left an extremely positive impression, above all because of the professionalism and the approach to solving the issues that arose.
Jitsi Meet: a personal Zoom — setup in Docker and on a VPS
2025-11-11 · ★ 5/5
I needed to get n8n, Redis, and the database working. I had hired another contractor before and everything kept breaking. I hired Mikhail, and the next day everything was working quickly, like clockwork!
There was a task to get n8n, redis and the database working. I had previously ordered from another contractor, it kept breaking all the time. Ordered from Mikhail, the next day everything started working fast, like …
n8n installation on your VPS server. Configuration of n8n, Docker, AI, Telegram
2025-09-24 · ★ 5/5
Thank you for the fast and excellent work. Everything was done promptly and just as needed!
Thank you for the quick and good work. Everything was done promptly and as needed!
n8n installation on your VPS server. Configuration of n8n, Docker, AI, Telegram
2025-09-06 · ★ 5/5
Quick solution — I highly recommend Mikhail as a contractor! I tried to build a similar configuration myself and even followed AI advice, which ended up costing a lot of time and money (due to server downtime). So my advice: hire professionals — it's cheaper =) Thanks to Mikhail for his professionalism.
Quick fix for the problem, I recommend Mikhail as a contractor to everyone! I tried to assemble a similar configuration myself and following advice from neural networks, which resulted in a lot of wasted effort and …
n8n installation on your VPS server. Configuration of n8n, Docker, AI, Telegram.
2025-08-25 · ★ 5/5
Mikhail completed the setup of another VPS. He quickly and professionally bypassed certain hosting providers' restrictions.
Mikhail completed the setup of another VPS. Quickly, professionally bypassing certain limitations of hosting providers.
n8n installation on your VPS server. n8n, Docker, AI, Telegram setup
2025-08-12 · ★ 5/5
Great job, thank you! Mikhail is a true professional — I recommend him!
Excellent work, thank you! Mikhail is a professional in his field, I recommend him!
N8n installation on your VPS server. Setup of n8n, Docker, AI, Telegram
2025-07-03 · ★ 5/5
// Contact
Need help?
Get in touch with me and I'll help solve the problem
// Related