Setup Docker Swarm
- This is a guide on setting up the NG Network
Prequisites
- Working VPN (Tailscale/Headscale) allowing ESP
- In order to use an encrypted overlay network Headscale must be configured to allow ESP (Protocol 50) through its tunnel. Sample ACL:
"acls": [
{
"Action": "accept",
"src": [
"*",
],
"proto": "tcp",
"dst": [
"*:*",
],
},
{
"Action": "accept",
"src": [
"*",
],
"proto": "udp",
"dst": [
"*:53",
],
},
{
"Action": "accept",
"src": [
"*",
],
"proto": "icmp",
"dst": [
"*:*",
],
},
{
"Action": "accept",
"src": [
"*",
],
"proto": "esp",
"dst": [
"*:*",
],
},
],
}
- Make sure A records exist for
$HOSTNAME
(Public IP) and$HOSTNAME.v
(VPN IP)
Setup Hosts (All)
-
Run arch-install.sh on easy mode and get a basic "Server" setup in place
- Create an
sdadmin
account when prompted to login locally on first boot
- Create an
-
Install and enable
tailscale
from package managerpacman -Syyu tailscale --noconfirm systemctl enable tailscale --now
-
Setup IPTables
- Add the following to the install scripts
/etc/iptables/iptables.rules
file
-A FILTERS -m state --state NEW -m tcp -p tcp -s $(tailscale ip --4) --dport 2377 -m comment --comment "Docker Swarm - Manager" -j ACCEPT -A FILTERS -m state --state NEW -m udp -p udp -s $(tailscale ip --4) --dport 4789 -m comment --comment "Docker Swarm - Ingress Traffic UDP" -j ACCEPT -A FILTERS -m state --state NEW -m tcp -p tcp -s $(tailscale ip --4) --dport 4789 -m comment --comment "Docker Swarm - Ingress Traffic TCP" -j ACCEPT -A FILTERS -m state --state NEW -m tcp -p tcp -s $(tailscale ip --4) --dport 7946 -m comment --comment "Docker Swarm - Container Network Discovery" -j ACCEPT
- Add the following to the install scripts
-
Delete all old docker networks from install script
docker network prune -f
-
Join the Host to the tailnet and follow the instructions visiting the link provided
tailscale up --login-server https://v.example.com
-
Authorize the connection on Headscale Control Server
headscale nodes register --user selfdesign --key nodekey:<whatever the nodekey is>
Setup Docker Swarm
Manager
Initialize the swarm First Manager Only
docker swarm init --listen $(tailscale ip --4) --advertise-addr $(tailscale ip --4)
- Delete unencrypted / MTU 1500
ingress
network
docker network rm ingress -f
- Create new docker overlay networks
# Recreate with encryption and set MTU to 1280 to support Tailscale VPN
docker network create --driver overlay --ingress --opt encrypted --opt com.docker.network.driver.mtu=1280 --subnet 10.10.0.0/16 --gateway 10.10.0.1 ingress
# host network for outside of docker
docker network create --subnet 10.11.0.0/16 --driver overlay --scope swarm --opt encrypted --attachable edge
# network hosting the socket proxy
docker network create --subnet 10.12.0.0/16 --driver overlay --scope swarm --opt encrypted --attachable socket-proxy
# network hosting the services that are routed by traefik
docker network create --subnet 10.13.0.0/16 --driver overlay --scope swarm --opt encrypted --attachable proxy-public
# network hosting the services that are routed by traefik
docker network create --subnet 10.14.0.0/16 --driver overlay --scope swarm --opt encrypted --attachable proxy-internal
- Add more managers
docker swarm join-token manager
Worker
-
On a manager node, get a join token
$ docker swarm join-token worker
-
Run the command given on a worker node eg
docker swarm join --token abcdef12345667 101.102.103.104:2377
Create Network Ingress
Socket Proxy
Create the following compose entry
/var/local/data/_system/socket-proxy/compose.yml
services:
socket-proxy:
image: docker.io/tiredofit/socket-proxy:latest
deploy:
placement:
constraints:
- node.labels.socket-proxy.proxy-public == true
- node.labels.socket-proxy.proxy-private == true
- node.role == manager
resources:
limits:
memory: 128m
restart_policy:
condition: on-failure
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./logs/socket-proxy:/logs
environment:
- TIMEZONE=America/Vancouver
- CONTAINER_NAME=socket-proxy
- CONTAINER_ENABLE_MONITORING=FALSE
- ALLOWED_IPS=127.0.0.1,10.12.0.0/16
- ENABLE_READONLY=TRUE
- MODE=containers,events,networks,ping,services,tasks,version
networks:
socket-proxy:
aliases:
- socket-proxy
networks:
socket-proxy:
external: true
- Start the stack
docker stack deploy -c /var/local/data/_system/socket-proxy/compose.yml socket-proxy
Public
This will create a Traefik Instance that is accessible to the public IPv4 internet.
On a manager node set the following tag so that the Socket Proxy is always deployed to the same node as our Traefik services and uses the same volume.
docker node update --label-add socket-proxy.proxy-public=true $(docker info -f '{{.Swarm.NodeID}}')
On a manager node set the following tag so that Traefik is always deployed to the same node and uses the same volume
docker node update --label-add proxy.proxy-public=true $(docker info -f '{{.Swarm.NodeID}}')
Create the following compose entry. note the differences in the deploy key and DOCKER_CONSTRAINTS, and networking (ports+overlay) compared to the public deployment
/var/local/data/_system/proxy-public/compose.yml
services:
traefik:
image: docker.io/tiredofit/traefik:2.10
deploy:
labels:
- traefik.constraint=proxy-public
placement:
constraints:
- node.labels.proxy.traefik-public-certificates == true
- node.role == manager
resources:
limits:
memory: 512m
restart_policy:
condition: on-failure
ports:
- 80:80
- 443:443
volumes:
- proxy-public-traefik-certificates:/data/certs
- proxy-public-traefik-logs:/data/logs
environment:
- TIMEZONE=America/Vancouver
- CONTAINER_NAME=traefik
- CONTAINER_ENABLE_MONITORING=FALSE
- ENABLE_DASHBOARD=FALSE
#- DASHBOARD_HOSTNAME=proxy-public.example.com
- DOCKER_ENDPOINT=http://socket-proxy:2375
- ENABLE_DOCKER_SWARM_MODE=TRUE
- DOCKER_CONTEXT=Label(`traefik.constraint`, `traefik-public`)
- DOCKER_DEFAULT_NETWORK=proxy-public
- ACCESS_LOG_TYPE=FILE
- LOG_TYPE=FILE
- TRAEFIK_USER=traefik
- LETSENCRYPT_EMAIL=zonemaster@example.com
- LETSENCRYPT_CHALLENGE=DNS
- LETSENCRYPT_DNS_PROVIDER=cloudflare
- CF_API_EMAIL=zonemaster@example.com
- CF_API_KEY=token
networks:
edge:
aliases:
- traefik-public-edge
proxy-public:
aliases:
- traefik-public
socket-proxy:
aliases:
- traefik-public-socket-proxy
cloudflare-companion:
image: docker.io/tiredofit/traefik-cloudflare-companion:latest
deploy:
placement:
constraints:
- node.labels.proxy.traefik-public-certificates == true
- node.role == manager
resources:
limits:
memory: 128m
restart_policy:
condition: on-failure
volumes:
- proxy-public-tcc-logs:/logs
environment:
- TIMEZONE=America/Vancouver
- CONTAINER_NAME=cloudflare-companion
- CONTAINER_ENABLE_LOGSHIPPING=FALSE
- CONTAINER_ENABLE_MONITORING=FALSE
- TCC_USER=tcc
- DOCKER_HOST=http://socket-proxy:2375
- TRAEFIK_VERSION=2
- CF_EMAIL=zonemaster@example.com
- CF_TOKEN={{CLOUDFLARE_TOKEN}}
- TARGET_DOMAIN={{HOSTNAME}}.example.com
- TRAEFIK_FILTER=proxy-public
- REFRESH_ENTRIES=TRUE
- DOMAIN1=example.com
- DOMAIN1_ZONE_ID=b44d3be5b2a3e526b2c57842d26d926e
networks:
socket-proxy:
aliases:
- cloudflare-companion-public
volumes:
proxy-public-traefik-certs:
proxy-public-traefik-logs:
proxy-public-tcc-logs:
networks:
edge:
external: true
proxy-public:
external: true
socket-proxy:
external: true
- Start the stack
docker stack deploy -c /var/local/data/_system/proxy-public/compose.yml proxy-public
Private
This will create a private traefik instance that is only accessible via the VPN.
On a manager node set the following tag so that the Socket Proxy is always deployed to the same node as our Traefik services and uses the same volume.
docker node update --label-add socket-proxy.proxy-private=true $(docker info -f '{{.Swarm.NodeID}}')
On a manager node set the following tag so that Traefik is always deployed to the same node and uses the same volume
docker node update --label-add proxy.proxy-private=true $(docker info -f '{{.Swarm.NodeID}}')
Create the following compose entry note the differences in the deploy key and DOCKER_CONSTRAINTS, and networking compared to the public one
/var/local/data/_system/proxy-private/compose.yml
services:
traefik:
image: docker.io/tiredofit/traefik:2.9
deploy:
placement:
constraints:
- node.labels.proxy.traefik-private-certificates == true
- node.role == manager
resources:
limits:
memory: 512m
restart_policy:
condition: on-failure
ports:
- 80:80
- 443:443
volumes:
- proxy-private-traefik-certificates:/data/certs
- proxy-private-traefik-logs:/data/logs
environment:
- TIMEZONE=America/Vancouver
- CONTAINER_NAME=traefik
- CONTAINER_ENABLE_MONITORING=FALSE
- ENABLE_DASHBOARD=FALSE
#- DASHBOARD_HOSTNAME=proxy-private.example.com
- DOCKER_ENDPOINT=http://socket-proxy:2375
- ENABLE_DOCKER_SWARM_MODE=TRUE
- DOCKER_CONTEXT=Label(`traefik.constraint`, `proxy-private`)
- DOCKER_DEFAULT_NETWORK=proxy-private
- ACCESS_LOG_TYPE=FILE
- LOG_TYPE=FILE
- TRAEFIK_USER=traefik
- LETSENCRYPT_EMAIL=zonemaster@example.com
- LETSENCRYPT_CHALLENGE=DNS
- LETSENCRYPT_DNS_PROVIDER=cloudflare
- CF_API_EMAIL=zonemaster@example.com
- CF_API_KEY=token
networks:
edge:
aliases:
- traefik-private-edge
proxy-private:
aliases:
- traefik-private
socket-proxy:
aliases:
- traefik-private-socket-proxy
cloudflare-companion:
image: docker.io/tiredofit/traefik-cloudflare-companion:latest
deploy:
placement:
constraints:
- node.labels.proxy.traefik-private-certificates == true
- node.role == manager
resources:
limits:
memory: 128m
restart_policy:
condition: on-failure
volumes:
- proxy-private-tcc-logs:/logs
environment:
- TIMEZONE=America/Vancouver
- CONTAINER_NAME=cloudflare-companion
- CONTAINER_ENABLE_MONITORING=FALSE
- TCC_USER=tcc
- DOCKER_HOST=http://socket-proxy:2375
- TRAEFIK_VERSION=2
- CF_EMAIL=zonemaster@example.com
- CF_TOKEN={{CLOUDFLARE_TOKEN}}
- TARGET_DOMAIN={{HOSTNAME}}.v.example.com
- TRAEFIK_FILTER=proxy-public
- REFRESH_ENTRIES=TRUE
- DOMAIN1=example.com
- DOMAIN1_ZONE_ID=b44d3be5b2a3e526b2c57842d26d926e
networks:
socket-proxy:
aliases:
- cloudflare-companion-private
volumes:
proxy-private-traefik-certs:
proxy-private-traefik-logs:
proxy-private-tcc-logs:
networks:
edge:
external: true
proxy-private:
external: true
socket-proxy:
external: true
- Start the stack
docker stack deploy -c /var/local/data/_system/proxy-private/compose.yml proxy-private
Create services
Network Scope
Public (Nginx Webserver)
This will create a simple service that will be deployed on a worker node. Note the label traefik.constraint
which limits it to be only used on the proxy-public
proxy instance.
On a manager create the following file:
/var/local/data/nginx.example.com/compose.yml
services:
nginx:
image: docker.io/tiredofit/nginx:latest
deploy:
labels:
- traefik.enable=true
- traefik.http.routers.nginx-example-com.rule=Host(`nginx.example.com`)
- traefik.http.services.nginx-example-com.loadbalancer.server.port=80
- traefik.constraint=proxy-public
replicas: 1
resources:
limits:
memory: 128m
restart_policy:
condition: on-failure
volumes:
- nginx-example-com-data:/www/html
- nginx-example-com-logs:/www/logs/nginx/
environment:
- TIMEZONE=America/Vancouver
- CONTAINER_ENABLE_MONITORING=FALSE
- CONTAINER_NAME=nginx-example-com-nginx
networks:
- proxy-public
volumes:
nginx-example-com-data:
nginx-example-com-logs:
networks:
proxy-public:
external: true
Private (Nginx Webserver)
This will create a simple service that will be deployed on a worker node. Note the label traefik.constraint
which limits it to be only used on the proxy-private
proxy instance.
On a manager create the following file:
/var/local/data/nginx.v.example.com/compose.yml
services:
nginx:
image: docker.io/tiredofit/nginx:latest
deploy:
labels:
- traefik.enable=true
- traefik.http.routers.nginx-v-example-com.rule=Host(`nginx.v.example.com`)
- traefik.http.services.nginx-v-example-com.loadbalancer.server.port=80
- traefik.constraint=proxy-private
replicas: 1
resources:
limits:
memory: 128m
restart_policy:
condition: on-failure
volumes:
- nginx-v-example-com-data:/www/html
- nginx-v-example-com-logs:/www/logs/nginx/
environment:
- TIMEZONE=America/Vancouver
- CONTAINER_ENABLE_MONITORING=FALSE
- CONTAINER_NAME=nginx-example-com-nginx
networks:
- proxy-private
volumes:
nginx-v-example-com-data:
nginx-v-example-com-logs:
networks:
proxy-private:
external: true
Full stack Application
Wordpress Basic
This will create a basic wordpress instance service that will be deployed on a worker node.
On a manager create /var/local/data/wordpress.example.com/compose.yml
services:
wordpress:
image: docker.io/tiredofit/wordpress:latest
deploy:
labels:
- traefik.enable=true
- traefik.http.routers.wordpress-example-com.rule=Host(`wordpress.example.com`)
- traefik.http.services.wordpress-example-com.loadbalancer.server.port=80
- traefik.constraint=proxy-public
replicas: 1
resources:
limits:
memory: 512m
restart_policy:
condition: on-failure
volumes:
- wordpress-example-com-data:/www/wordpress
- wordpress-example-com-logs:/www/logs
environment:
- TIMEZONE=America/Vancouver
- CONTAINER_ENABLE_MONITORING=FALSE
- CONTAINER_NAME=wordpress
- DB_HOST=wordpress-db
- DB_NAME=wordpress
- DB_USER=wordpress
- DB_PASS=userpassword
- ENABLE_HTTPS_REVERSE_PROXY=FALSE
- ADMIN_EMAIL=email@example.com
- ADMIN_USER=admin
- SITE_URL=https://wordpress.example.com
- SITE_TITLE=Docker Wordpress
networks:
- proxy-public
wordpress-db:
image: docker.io/tiredofit/mariadb
deploy:
replicas: 1
resources:
limits:
memory: 512m
restart_policy:
condition: on-failure
volumes:
- wordpress-example-com-db:/var/lib/mysql
environment:
- TIMEZONE=America/Vancouver
- CONTAINER_NAME=wordpress-db
- ROOT_PASS=password
- DB_NAME=wordpress
- DB_USER=wordpress
- DB_PASS=userpassword
networks:
- proxy-public
volumes:
wordpress-example-com-data:
wordpress-example-com-logs:
networks:
proxy-public:
external: true
Wordpress Advanced
This advanced setup splits frontend and backend duties into their own containers for scalability purposes.
On a manager create /var/local/data/advanced.wordpress.example.com/compose.yml
services:
frontend:
image: docker.io/tiredofit/wordpress:latest
deploy:
labels:
- traefik.enable=true
- traefik.http.routers.advanced-wordpress-example-com.rule=Host(`advanced-wordpress.example.com`)
- traefik.http.services.advanced-wordpress-example-com.loadbalancer.server.port=80
- traefik.constraint=proxy-public
replicas: 1
resources:
limits:
memory: 512m
restart_policy:
condition: on-failure
volumes:
- advanced-wordpress-example-com-data:/www/wordpress
- advanced-wordpress-example-com-logs:/www/logs
environment:
- TIMEZONE=America/Vancouver
- CONTAINER_ENABLE_MONITORING=FALSE
- CONTAINER_NAME=wordpress
- PHP_FPM_CONTAINER_MODE=nginx
- PHP_FPM_CONTAINER_MODE=advanced-wordpress-selfdedsign-org-backend
- DB_HOST=advanced-wordpress-db
- DB_NAME=wordpress
- DB_USER=wordpress
- DB_PASS=userpassword
- ADMIN_EMAIL=email@example.com
- ADMIN_USER=admin
- SITE_URL=https://advanced.wordpress.example.com
- SITE_TITLE=Docker Wordpress Advanced
networks:
- proxy-public
backend:
image: docker.io/tiredofit/wordpress:latest
deploy:
replicas: 1
resources:
limits:
memory: 512m
restart_policy:
condition: on-failure
volumes:
- advanced-wordpress-example-com-data:/www/wordpress
- advanced-wordpress-example-com-logs:/www/logs
environment:
- TIMEZONE=America/Vancouver
- CONTAINER_ENABLE_MONITORING=FALSE
- CONTAINER_NAME=backend
- PHP_FPM_CONTAINER_MODE=php-fpm
networks:
proxy-public:
aliases:
- advanced-wordpress-example-com-backend
db:
image: docker.io/tiredofit/mariadb:10.11
deploy:
replicas: 1
resources:
limits:
memory: 512m
restart_policy:
condition: on-failure
volumes:
- advanced-wordpress-example-com-db:/var/lib/mysql
environment:
- TIMEZONE=America/Vancouver
- CONTAINER_NAME=advanced-wordpress-db
- ROOT_PASS=password
- DB_NAME=wordpress
- DB_USER=wordpress
- DB_PASS=userpassword
networks:
- proxy-public
volumes:
advanced-wordpress-example-com-data:
advanced-wordpress-example-com-logs:
advanced-wordpress-example-com-db:
networks:
proxy-public:
external: true
TODO
- Switch the example to named volumes
- Drop Logs and switch to fluent-bit logshipping
- Build healthchecks example
- Create Ingress constraints example for private and public services
- Create broken out NPFPM example
- Create Gallera Example
- Create Redis session cache
- Ceph / Distributed Volumes