Skip to content

Adding a New Service

How to deploy a new application to the Docker Swarm cluster.

Prerequisites

  • Source code on apps-dev1 (192.168.51.40) in /opt/development/
  • Docker image builds successfully
  • DNS configured (Cloudflare for public, UDM for internal)

Step 1: Create Dockerfile

On apps-dev1, create a production Dockerfile:

FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --production

FROM node:22-alpine
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]

Step 2: Build and Push

# On apps-dev1
cd /opt/development/myapp
REGISTRY=registry.apps.jlwaller.com

docker build -t ${REGISTRY}/myapp:latest .
docker push ${REGISTRY}/myapp:latest

# Verify
curl -sk https://${REGISTRY}/v2/myapp/tags/list

Step 3: Create Stack File

On apps-gateway, create the stack file:

mkdir -p /opt/swarm/stacks/app-myapp

/opt/swarm/stacks/app-myapp/stack.myapp.yml:

version: '3.9'

services:
  myapp:
    image: registry.apps.jlwaller.com/myapp:latest
    networks:
      - public_net
      - data_net
    deploy:
      replicas: 1
      placement:
        constraints:
          - node.role == worker
      update_config:
        parallelism: 1
        delay: 10s
        failure_action: rollback
        order: start-first
      restart_policy:
        condition: any
        delay: 5s
        max_attempts: 5
      resources:
        limits:
          memory: 256M
          cpus: '0.5'
        reservations:
          memory: 128M
          cpus: '0.25'
      labels:
        - "traefik.enable=true"
        - "traefik.http.routers.myapp.rule=Host(`myapp.jlwaller.com`)"
        - "traefik.http.routers.myapp.entrypoints=websecure"
        - "traefik.http.routers.myapp.tls.certresolver=letsencrypt"
        - "traefik.http.services.myapp.loadbalancer.server.port=3000"
        - "traefik.docker.network=public_net"

networks:
  public_net:
    external: true
  data_net:
    external: true

Key Configuration Notes

  • Networks: Use public_net for Traefik routing, data_net if the service needs database/cache access
  • Traefik labels: Must be under deploy.labels (not service-level labels) for Swarm mode
  • Port: Match the port your application listens on
  • docker.network label: Required if the service is on multiple networks

Step 4: Configure DNS

For public domains (via Cloudflare):

  1. Add A record pointing to the Cloudflare proxy
  2. Enable orange cloud (proxied)
  3. Traefik will auto-obtain a Let's Encrypt certificate

For internal domains (via UDM):

  1. Add A record pointing to 192.168.50.10
  2. No Cloudflare proxy needed

Step 5: Deploy

docker stack deploy -c /opt/swarm/stacks/app-myapp/stack.myapp.yml app-myapp

Step 6: Verify

# Check service is running
docker service ls | grep myapp
docker service ps app-myapp_myapp

# Check logs
docker service logs -f app-myapp_myapp

# Check Traefik picked up the route
curl http://localhost:8080/api/http/routers | python3 -c "import sys,json; [print(r['rule']) for r in json.load(sys.stdin) if 'myapp' in r.get('name','')]"

# Test endpoint
curl -I https://myapp.jlwaller.com

Adding Database Access

If the service needs a database:

  1. Create the database:

    docker exec -it $(docker ps -qf name=data_postgres) psql -U postgres -c "CREATE DATABASE myapp;"
    docker exec -it $(docker ps -qf name=data_postgres) psql -U postgres -c "CREATE USER myapp_owner WITH PASSWORD 'secure_password';"
    docker exec -it $(docker ps -qf name=data_postgres) psql -U postgres -c "GRANT ALL ON DATABASE myapp TO myapp_owner;"
    

  2. Create Docker secret:

    echo "postgresql://myapp_owner:secure_password@pgbouncer:6432/myapp" | docker secret create myapp_database_url -
    

  3. Add to stack file:

    services:
      myapp:
        secrets:
          - myapp_database_url
        environment:
          DATABASE_URL_FILE: /run/secrets/myapp_database_url
    
    secrets:
      myapp_database_url:
        external: true
    

Adding BasicAuth Protection

Use the existing staging-auth middleware or create a new one:

deploy:
  labels:
    - "traefik.http.middlewares.myapp-auth.basicauth.users=user:$$2y$$05$$hashedpassword"
    - "traefik.http.routers.myapp.middlewares=myapp-auth"

Generate the hash: htpasswd -nbB username password