Deploying Yii applications to Docker Swarm and Traefik
This guide walks you through deploying a Yii application to Docker Swarm starting from a blank server, using Traefik as a reverse proxy and deploying from a container registry (Forgejo or Gitea).
graph LR
A[Internet] --> B[Reverse Proxy: Traefik]
B --> C[app1.example.com]
B --> D[app2.example.com]
subgraph Docker Swarm Cluster
B
C
D
endPrerequisites
- A server with a fresh installation of a Linux distribution (Ubuntu 22.04 LTS or later recommended)
- A domain name pointing to your server's IP address
- SSH access to your server
- Basic knowledge of Docker and command-line tools
Server preparation
Install Docker
For installation instructions, see the official Docker documentation.
Initialize Docker Swarm
Initialize your server as a Docker Swarm manager:
docker swarm init --advertise-addr <YOUR_SERVER_IP>Replace <YOUR_SERVER_IP> with your server's public IP address.
Set up the reverse proxy network
Create a dedicated overlay network for reverse proxy to communicate with your services:
docker network create --driver=overlay reverse_proxy_publicSetting up Traefik as reverse proxy
To deploy Traefik as reverse proxy create a file traefik-stack.yml:
services:
traefik:
image: traefik:v2.10
command:
- "--api.insecure=true"
- "--providers.docker=true"
- "--providers.docker.swarmMode=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.letsencrypt.acme.tlschallenge=true"
- "--certificatesresolvers.letsencrypt.acme.email=admin@example.com"
- "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
ports:
- "80:80"
- "443:443"
- "8080:8080"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
- "traefik_certificates:/letsencrypt"
networks:
- reverse_proxy_public
deploy:
placement:
constraints:
- node.role == manager
volumes:
traefik_certificates:
networks:
reverse_proxy_public:
external: trueDeploy Traefik:
docker stack deploy -c traefik-stack.yml traefikSetting up a container registry
You need a container registry to store your Docker images. Choose one of the following options.
Option 1: Using Forgejo
To deploy Forgejo create a file forgejo-stack.yml:
services:
forgejo:
image: codeberg.org/forgejo/forgejo:1.21
ports:
- "3000:3000"
volumes:
- forgejo_data:/data
networks:
- reverse_proxy_public
deploy:
labels:
- "traefik.enable=true"
- "traefik.http.routers.app.rule=Host(`git.example.com`)"
- "traefik.http.routers.app.entrypoints=websecure"
- "traefik.http.routers.app.tls.certresolver=letsencrypt"
- "traefik.http.services.app.loadbalancer.server.port=3000"
volumes:
forgejo_data:
networks:
reverse_proxy_public:
external: trueDeploy Forgejo:
docker stack deploy -c forgejo-stack.yml forgejoReplace git.example.com with your desired subdomain.
After deployment, access Forgejo at https://git.example.com and complete the initial setup. Make sure to enable the container registry in the settings.
Option 2: Using Gitea
To deploy Gitea create a file gitea-stack.yml:
services:
gitea:
image: gitea/gitea:latest
ports:
- "3000:3000"
volumes:
- gitea_data:/data
networks:
- reverse_proxy_public
deploy:
labels:
- "traefik.enable=true"
- "traefik.http.routers.app.rule=Host(`git.example.com`)"
- "traefik.http.routers.app.entrypoints=websecure"
- "traefik.http.routers.app.tls.certresolver=letsencrypt"
- "traefik.http.services.app.loadbalancer.server.port=3000"
volumes:
gitea_data:
networks:
reverse_proxy_public:
external: trueDeploy Gitea:
docker stack deploy -c gitea-stack.yml giteaReplace git.example.com with your desired subdomain.
After deployment, access Gitea at https://git.example.com and complete the initial setup. Make sure to enable the container registry in the settings.
Configuring your Yii application
Update the Makefile configuration
The Yii application template includes a Makefile with deployment commands. Update the docker/.env file in your project:
STACK_NAME=myapp
#
# Production
#
PROD_HOST=app.example.com
PROD_SSH="ssh://user@your-server-ip"
IMAGE=git.example.com/username/myapp
IMAGE_TAG=latestReplace the values:
STACK_NAME: A unique name for your application stackPROD_HOST: The domain name where your app will be accessiblePROD_SSH: SSH connection string to your server (format:ssh://user@host)IMAGE: Full path to your container image in the registryIMAGE_TAG: Image tag, typicallylatestor a version number
Configure the production environment
Update docker/prod/.env with your production environment variables:
APP_ENV=prod
YII_DEBUG=false
YII_ENV=prod
# Database configuration
DB_HOST=db
DB_NAME=myapp
DB_USER=myapp
DB_PASSWORD=secure_password_here
# Add other environment-specific variablesWARNING
Never commit sensitive credentials to version control. Use docker/prod/override.env for sensitive values and add it to .gitignore.
Review the production Docker Compose configuration
When using Traefik, you'll need to modify the Yii application's docker/prod/compose.yml to use Traefik labels instead of Caddy labels:
services:
app:
image: ${IMAGE}:${IMAGE_TAG}
networks:
- reverse_proxy_public
volumes:
- runtime:/app/runtime
- caddy_data:/data
- caddy_config:/config
env_file:
- path: ./prod/.env
- path: ./prod/override.env
required: false\
environment:
CADDY_EXTRA_CONFIG: 'auto_https off'
deploy:
replicas: 2
update_config:
delay: 10s
parallelism: 1
order: start-first
failure_action: rollback
monitor: 10s
rollback_config:
parallelism: 0
order: stop-first
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 3
window: 120s
labels:
- "traefik.enable=true"
- "traefik.http.routers.app.rule=Host(`${PROD_HOST:-app.example.com}`)"
- "traefik.http.routers.app.entrypoints=websecure"
- "traefik.http.routers.app.tls.certresolver=letsencrypt"
- "traefik.http.services.app.loadbalancer.server.port=80"This configuration:
- Runs 2 replicas for high availability
- Uses a rolling update strategy with automatic rollback on failure
- Configures
labelsfor automatic HTTPS on the reverse proxy - Disables obtaining of HTTPs certificates on the container itself since proxy communicates with the container via HTTP. That is
auto_https off.
If you need a database, add it to the stack:
services:
app:
# ... existing configuration ...
db:
image: postgres:15-alpine
environment:
POSTGRES_DB: myapp
POSTGRES_USER: myapp
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
volumes:
- db_data:/var/lib/postgresql/data
networks:
- reverse_proxy_public
deploy:
placement:
constraints:
- node.role == manager
secrets:
- db_password
volumes:
runtime:
db_data:
secrets:
db_password:
external: trueCreate the database password secret on the server:
echo "your_secure_password" | docker secret create db_password -Building and pushing the image
Set up Docker login on your local machine
Configure Docker to authenticate with your container registry:
docker login git.example.comEnter your username and password when prompted.
Build the production image
Use the Makefile to build your production image:
make prod-buildThis runs the command defined in the Makefile:
docker build --file docker/Dockerfile --target prod --pull -t ${IMAGE}:${IMAGE_TAG} .The Dockerfile uses a multi-stage build:
- Installs Composer dependencies in a builder stage
- Creates a minimal production image with only the necessary files
- Runs as a non-root user (
www-data)
Push the image to the registry
Push your built image to the container registry:
make prod-pushThis executes:
docker push ${IMAGE}:${IMAGE_TAG}Deploying to Docker Swarm
Configure SSH access
Set up SSH key-based authentication to your server:
# Generate SSH key (if you don't have one)
ssh-keygen -t ed25519 -C "your_email@example.com"
# Copy the key to your server
ssh-copy-id user@your-server-ip
# Add the SSH host to your SSH config (~/.ssh/config)
cat >> ~/.ssh/config << EOF
Host docker-web
HostName your-server-ip
User user
IdentityFile ~/.ssh/id_ed25519
EOFSet up Docker context
Create a Docker context for remote deployment:
docker context create swarm-prod --docker "host=ssh://docker-web"Alternatively, configure the DOCKER_HOST environment variable:
export DOCKER_HOST=ssh://docker-webDeploy the application
Deploy your application stack to Docker Swarm:
make prod-deployThis executes:
docker -H ${PROD_SSH} stack deploy --prune --detach=false --with-registry-auth -c docker/compose.yml -c docker/prod/compose.yml ${STACK_NAME}The --with-registry-auth flag ensures the Swarm nodes can pull images from your private registry.
Verify the deployment
Check the status of your services:
docker -H ssh://docker-web service ls
docker -H ssh://docker-web service ps ${STACK_NAME}_appView logs:
docker -H ssh://docker-web service logs ${STACK_NAME}_appMonitoring and maintenance
View service logs
# View all logs
docker -H ssh://docker-web service logs -f ${STACK_NAME}_app
# View logs from the last 100 lines
docker -H ssh://docker-web service logs --tail 100 ${STACK_NAME}_app
# View logs with timestamps
docker -H ssh://docker-web service logs -t ${STACK_NAME}_appScale the application
Adjust the number of replicas:
docker -H ssh://docker-web service scale ${STACK_NAME}_app=3Or update the replicas value in docker/prod/compose.yml and redeploy.
Resource limits
Add resource limits to prevent containers from consuming all server resources. Update docker/prod/compose.yml:
services:
app:
# ... existing configuration ...
deploy:
resources:
limits:
cpus: '0.5'
memory: 512M
reservations:
cpus: '0.25'
memory: 256MSecurity considerations
Use Docker secrets for sensitive data
Instead of environment variables, use Docker secrets for sensitive information:
# Create secrets
echo "database_password" | docker secret create db_password -
echo "api_key" | docker secret create api_key -Update docker/prod/compose.yml:
services:
app:
secrets:
- db_password
- api_key
secrets:
db_password:
external: true
api_key:
external: trueAccess secrets in your application at /run/secrets/secret_name.
Set up a firewall
Configure UFW (Uncomplicated Firewall) on your server:
# Allow SSH
sudo ufw allow 22/tcp
# Allow HTTP and HTTPS
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
# Allow Docker Swarm ports (if you plan to add more nodes)
sudo ufw allow 2377/tcp
sudo ufw allow 7946/tcp
sudo ufw allow 7946/udp
sudo ufw allow 4789/udp
# Enable the firewall
sudo ufw enableKeep the system updated
Regularly update your server and Docker:
# Update system packages
sudo apt-get update && sudo apt-get upgrade -y
# Update Docker images
docker -H ssh://docker-web service update --image ${IMAGE}:${IMAGE_TAG} ${STACK_NAME}_appTroubleshooting
Service won't start
Check service events and logs:
docker -H ssh://docker-web service ps ${STACK_NAME}_app --no-trunc
docker -H ssh://docker-web service logs ${STACK_NAME}_appCommon issues:
- Image pull errors: Verify registry authentication with
docker -H ssh://docker-web login - Port conflicts: Ensure no other services are using ports 80/443
- Resource constraints: Check available resources with
docker -H ssh://docker-web node ls
SSL certificate issues
If Traefik can't obtain certificates:
- Verify DNS is pointing to your server
- Check that ports 80 and 443 are accessible from the internet
- Ensure the email in the Let's Encrypt configuration is valid
- Check logs:
docker -H ssh://docker-web service logs traefik_traefik
Container registry connection issues
Test registry connectivity:
# From your local machine
docker pull git.example.com/username/myapp:latest
# From the server
docker -H ssh://docker-web pull git.example.com/username/myapp:latestSummary
You've successfully deployed a Yii application to Docker Swarm with:
- A container registry (Forgejo or Gitea)
- Automatic HTTPS via Traefik
- Zero-downtime deployments with rolling updates
- High availability with multiple replicas
The Makefile commands simplify the deployment workflow:
make prod-build- Build the production imagemake prod-push- Push to the registrymake prod-deploy- Deploy to Docker Swarm
For more information, see: