docs

Various Yii 3.0 related documentation

View the Project on GitHub yiisoft/docs

Deploying Yii applications to Docker Swarm

This guide walks you through deploying a Yii application to Docker Swarm from a blank server, using Caddy as a reverse proxy and a container registry (Forgejo or Gitea).

graph LR
    A[Internet] --> B[Reverse Proxy: Caddy or Traefik]
    B --> C[app1.example.com]
    B --> D[app2.example.com]
    
    subgraph Docker Swarm Cluster
        B
        C
        D
    end

Prerequisites

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 Caddy network

Create a dedicated overlay network for Caddy to communicate with your services:

docker network create --driver=overlay caddy_public

Setting up a container registry

You need a container registry to store your Docker images. Choose one of the following options.

Option 1: Using Forgejo

Deploy Forgejo as a container registry.

Create a file forgejo-stack.yml:

version: '3.8'

services:
  forgejo:
    image: codeberg.org/forgejo/forgejo:1.21
    ports:
      - "3000:3000"
    volumes:
      - forgejo_data:/data
    networks:
      - caddy_public
    deploy:
      labels:
        - "caddy=git.example.com"
        - "caddy.reverse_proxy="

volumes:
  forgejo_data:

networks:
  caddy_public:
    external: true

Deploy Forgejo:

docker stack deploy -c forgejo-stack.yml forgejo

Replace 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

Deploy Gitea as a container registry.

Create a file gitea-stack.yml:

version: '3.8'

services:
  gitea:
    image: gitea/gitea:latest
    ports:
      - "3000:3000"
    volumes:
      - gitea_data:/data
    networks:
      - caddy_public
    deploy:
      labels:
        - "caddy=git.example.com"
        - "caddy.reverse_proxy="

volumes:
  gitea_data:

networks:
  caddy_public:
    external: true

Deploy Gitea:

docker stack deploy -c gitea-stack.yml gitea

Replace 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.

Setting up Caddy as reverse proxy

The Yii application template includes Caddy labels by default. Deploy Caddy with automatic HTTPS.

Create a file caddy-stack.yml:

version: '3.8'

services:
  caddy:
    image: lucaslorentz/caddy-docker-proxy:ci-alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - caddy_data:/data
    networks:
      - caddy_public

volumes:
  caddy_data:

networks:
  caddy_public:
    external: true

Deploy Caddy:

docker stack deploy -c caddy-stack.yml caddy

Caddy automatically discovers services with Caddy labels and sets up HTTPS using Let’s Encrypt.

[!IMPORTANT] Make sure your domain DNS records are configured and pointing to your server before deploying services with Caddy labels, as Let’s Encrypt requires domain validation.

Alternative: Using Traefik as reverse proxy

If you prefer Traefik over Caddy, deploy it as follows:

Create a file traefik-stack.yml:

version: '3.8'

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:
      - caddy_public
    deploy:
      placement:
        constraints:
          - node.role == manager

volumes:
  traefik_certificates:

networks:
  caddy_public:
    external: true

Deploy Traefik:

docker stack deploy -c traefik-stack.yml traefik

If using Traefik, you’ll need to modify the Yii application’s docker/prod/compose.yml to use Traefik labels instead of Caddy labels.

Example Traefik labels for your application:

deploy:
  labels:
    - "traefik.enable=true"
    - "traefik.http.routers.app.rule=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"

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=latest

Replace the values:

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 variables

[!WARNING] 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

The default docker/prod/compose.yml includes:

services:
  app:
    image: ${IMAGE}:${IMAGE_TAG}
    networks:
      - caddy_public
    volumes:
      - runtime:/app/runtime
      - caddy_data:/data
      - caddy_config:/config
    env_file:
      - path: ./prod/.env
      - path: ./prod/override.env
        required: false
    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:
        caddy: ${PROD_HOST:-app.example.com}
        caddy.reverse_proxy: ""

This configuration:

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:
      - caddy_public
    deploy:
      placement:
        constraints:
          - node.role == manager
    secrets:
      - db_password

volumes:
  runtime:
  db_data:

secrets:
  db_password:
    external: true

Create 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.com

Enter your username and password when prompted.

Build the production image

Use the Makefile to build your production image:

make prod-build

This 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:

  1. Installs Composer dependencies in a builder stage
  2. Creates a minimal production image with only the necessary files
  3. Runs as a non-root user (www-data)

Push the image to the registry

Push your built image to the container registry:

make prod-push

This 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
EOF

Set 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-web

Deploy the application

Deploy your application stack to Docker Swarm:

make prod-deploy

This executes:

docker -H ${PROD_SSH} stack deploy --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}_app

View logs:

docker -H ssh://docker-web service logs ${STACK_NAME}_app

Monitoring 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}_app

Scale the application

Adjust the number of replicas:

docker -H ssh://docker-web service scale ${STACK_NAME}_app=3

Or 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: 256M

Security 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: true

Access 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 enable

Keep 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}_app

Troubleshooting

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}_app

Common issues:

SSL certificate issues

If Caddy/Traefik can’t obtain certificates:

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:latest

Summary

You’ve successfully deployed a Yii application to Docker Swarm with:

The Makefile commands simplify the deployment workflow:

For more information, see: