Skip to content

Commit a4242c4

Browse files
committed
Initial commit for publishing.
0 parents  commit a4242c4

27 files changed

+2398
-0
lines changed

Dockerfile

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# syntax = docker/dockerfile:1-experimental
2+
# Dockerfile for building the project with static assets
3+
FROM golang:1.21-bullseye as build
4+
5+
WORKDIR /goapp
6+
7+
RUN --mount=type=cache,target=/root/.cache/go-build GOOS=${TARGETOS} \
8+
GOARCH=${TARGETARCH} \
9+
go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest
10+
11+
RUN --mount=type=cache,target=/root/.cache/go-build GOOS=${TARGETOS} \
12+
GOARCH=${TARGETARCH} \
13+
xcaddy build \
14+
--with github.com/gamalan/caddy-tlsredis \
15+
--with github.com/lucaslorentz/caddy-docker-proxy/v2
16+
17+
# Now copy it into our base image.
18+
FROM alpine:latest as alpine
19+
EXPOSE 80 443 443/udp 2019
20+
ENV XDG_CONFIG_HOME /config
21+
ENV XDG_DATA_HOME /data
22+
RUN apk add -U --no-cache ca-certificates curl
23+
COPY --from=build /goapp/caddy /bin/caddy
24+
ENTRYPOINT ["/bin/caddy"]
25+
CMD ["docker-proxy"]

README.md

+174
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
# caddy-docker-proxy-redis
2+
3+
## How I do run my linux servers
4+
5+
I do have a few linux virtual servers spread around in various data
6+
centers. Originally that was mostly nginx front ends to various locally
7+
installed applications, all supervised by systemd. Changing to caddy
8+
as the proxy server was a fresh breaze as the configuration got much
9+
simpler and there where no shell scripts needed any longer to update
10+
certificates with letsencrypt. Still a lot installed on each server, but
11+
simpler.
12+
13+
My most recent iteration in making the administrative side easier is the
14+
following setup:
15+
16+
* All nodes run tailscale as an overlay VPN for admin functions.
17+
* All nodes run docker and one instance of the watchtower container
18+
for easy automatic upgrades of the individual packages.
19+
* Docker is configured to log to Google Cloud Logging for easy log file
20+
monitoring. This is may not be relevant for most people, but for me
21+
it makes perusing logs much easier.
22+
* All docker containers are started via a docker-compose.yml file. Each
23+
of those gets their own subdirectory with the docker-compose.yml
24+
file alongside any additional configuration and data volumes needed.
25+
* I run caddy with the docker-proxy and caddy-tls-redis plugins in a
26+
container as front end proxy.
27+
* Individual containers for the services use caddy docker proxy label
28+
fragments for configuration in the individual docker-compose.yml
29+
files.
30+
* All caddy instances share a common storage backend (a redis instance
31+
reached via the tailscale overlay vpn) for tls certificate storage.
32+
This facilitates easy moving a service from one server box to another
33+
without a lot of downtime due to missing TLS certificates.
34+
* I use an instance of gatus to monitor all these services and get
35+
notified by email about any failures. The caddy health check is on
36+
the tailscale side, so I check both the status of the tailnet as
37+
well as caddy basics working in one check.
38+
39+
All the referenced docker-compose files below assume a docker bridge
40+
network named caddy, see the networks.sh script.
41+
42+
I do override configuration on the docker service via creating the
43+
directory /etc/systemd/system/docker.service.d and creating a file
44+
override.conf like this:
45+
46+
```
47+
[Service]
48+
After=tailscaled.service
49+
Environment="GOOGLE_APPLICATION_CREDENTIALS=/home/amdinuser/.serviceaccts/hosting-XXXXXX-XXXXXXXXXXXX.json"
50+
```
51+
52+
The After= section makes sure that docker starts after tailscale is
53+
innitialized. GOOGLE_APPLICATION_CREDENTIALS injects the credentials of
54+
a service account that has log and error reporting permissions on a
55+
Google Cloud project. I modify the docker daemon config in
56+
/etc/docker/dameon.json like this:
57+
58+
```
59+
{
60+
"log-driver": "gcplogs",
61+
"log-opts": {
62+
"gcp-project": "hosting-XXXXXX",
63+
"gcp-meta-name": "myservername"
64+
}
65+
}
66+
```
67+
68+
The Google Cloud configuration is optional if you like to use journalctl
69+
on the individual hosts.
70+
71+
## Caddy
72+
73+
The root directory of this repo contains the Dockerfile and a
74+
build-docker.sh script to build the container that runs caddy with the
75+
docker proxy and tls-redis plugins. I do build both AMD64 and ARM64
76+
versions of each of my containers as my linux systems use both of these
77+
architectures. The caddy subdirectory showcases a typical caddy
78+
configuration. I do run caddy in its container with ports forwared for
79+
port 80 and 443 TCP and 443 UDP for QUIC aka HTTP/3. For easier
80+
configuration of the individual services I do include Caddyfile snippets
81+
in the config/Caddfile subdirectory. The caddy docker-proxy
82+
configuration to build a final Caddyfile can get a bit obscure for more
83+
complicated containers like nextcloud.
84+
85+
This Caddyfile also defines a https site on the tailscale side for the
86+
host, this has by default just a /health endpoint for health checking.
87+
The watchtower and adminer containers do add subdirectory endpoints here
88+
for the watchtower API and for examining database content.
89+
90+
The volume /run/containers for caddy might look unfamilar, but I use
91+
this to expose my own services via unix domain sockets. If you do not
92+
expect to proxy to upstream servers via unix domain sockets you might
93+
omit that volume.
94+
95+
## Watchtower
96+
97+
The container defined in the watchtower subdirectory is responsible for
98+
updating the containers actually running on the host, with the exception
99+
of watchtower itself. Lettings watchtower update itself does not appear
100+
to be working, but fortunately this is mature and changes seldomly.
101+
Please note that you should set a random password for the watchtower API
102+
in this docker-compose.yml. This container also needs to access your
103+
docker credentials in your home directory for accessing your docker
104+
repositories.
105+
106+
## Whoami
107+
108+
For debugging (and as a placeholder to aquire certificates) I tend to
109+
start an instance of whoami on the canonical host name of the linux box.
110+
For hosts that may be running postfix and dovecot or other services
111+
outside the caddy/docker universe a seperate project in
112+
github.com/jum/certwatch can be used to monitor a set of certificates in
113+
the caddy redis storage and write the certificates to /var/lib/certwatch
114+
and restart systemd services.
115+
116+
## Adminer
117+
118+
Adminer is a nice tool to examine mariadb or postgres databases. I do
119+
only run it exposed on the tailscale host name under the /adminer
120+
endpoint, and then only when I need it.
121+
122+
## Redis
123+
124+
One node in the tailscale VPN should run redis for storing the TLS
125+
certifcates. The redis subdirectory contains an example for this, please
126+
note that this also has to expose the tailnet side of the hosts IP (the
127+
100.X.X.X IP in the docker-compose.yml file for use in redis URLs.
128+
129+
## Databases
130+
131+
The postgres and mariadb subdorectories contain docker-compose files to
132+
start any of these databases. I have used mariadb in the past but on new
133+
projects I am mostly using postgres. Please note that for both databases
134+
you would need to change the IP numer 100.X.X:X to be able to access
135+
these databases from anywhere inside you tailnet.
136+
137+
## Backup
138+
139+
I do run nightly backups via restic to a Hetzner storage box via SFTP.
140+
After configuring one of those and putting these credentials into your
141+
.ssh/config and into /root/.ssh/config as well I run using a script like
142+
this:
143+
144+
```
145+
#!/bin/sh
146+
export PATH=$PATH:/usr/local/bin
147+
148+
if test -t 0
149+
then
150+
VERBOSE=
151+
else
152+
VERBOSE=-q
153+
fi
154+
export RESTIC_REPOSITORY=sftp:storage:backup-hostXXX/restic
155+
export RESTIC_PASSWORD_FILE=$HOME/.restic-backup-hostXXX
156+
157+
sudo -u adminuser sh -c "cd ~/web/redis; ./dumpdb.sh"
158+
sudo -u adminuser sh -c "cd ~/web/mariadb; ./dumpdb.sh"
159+
sudo -u adminuser sh -c "cd ~/web/postgres; ./dumpdb.sh"
160+
161+
restic $VERBOSE backup --exclude-caches=true \
162+
/etc/docker /var/lib/docker \
163+
/usr/local \
164+
/home/adminuser /root
165+
restic $VERBOSE forget \
166+
--keep-daily 7 --keep-weekly 5 --keep-monthly 12 \
167+
--keep-yearly 100 --prune
168+
169+
chown -R adminuser:adminuser /home/adminuser/.cache/restic
170+
```
171+
172+
This scripts assumes that the subdirectories for the docker containers
173+
are kept under ~adminuser/web and that the adminuser is a member of the
174+
docker group.

adminer/docker-compose.yml

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
name: adminer
2+
services:
3+
adminer:
4+
container_name: adminer
5+
restart: always
6+
image: adminer
7+
environment:
8+
ADMINER_DEFAULT_SERVER: postgres
9+
labels:
10+
caddy: "{env.CADDY_TAILNET_HOST}"
11+
caddy.handle_path: "/adminer/*"
12+
caddy.handle_path.reverse_proxy: "{{upstreams 8080}}"
13+
networks:
14+
default:
15+
name: caddy
16+
external: true

build_docker.sh

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#!/bin/sh
2+
DOCKER_PREFIX=jumager/caddy
3+
#branch=$(basename $(git rev-parse --abbrev-ref HEAD))
4+
branch=latest
5+
docker buildx build --platform linux/arm64,linux/amd64 -t ${DOCKER_PREFIX}:${branch} -f Dockerfile --push .

caddy/config/Caddyfile

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
{
2+
3+
cert_issuer acme
4+
default_sni {env.CADDY_HOST}
5+
storage redis {
6+
host "{env.CADDY_REDIS_HOST}"
7+
}
8+
log {
9+
output file /data/access.log
10+
}
11+
}
12+
13+
(defaulthdr) {
14+
header {
15+
Strict-Transport-Security "max-age=15552000; includeSubDomains; preload"
16+
-Server
17+
}
18+
encode zstd gzip
19+
log
20+
}
21+
22+
(robots) {
23+
import defaulthdr
24+
handle_path /robots.txt {
25+
file_server * {
26+
root /data/web/robots/robots.txt
27+
}
28+
}
29+
}
30+
31+
(norobots) {
32+
import defaulthdr
33+
handle_path /robots.txt {
34+
file_server * {
35+
root /data/robots/norobots.txt
36+
}
37+
}
38+
}
39+
40+
(naked) {
41+
import defaulthdr
42+
redir https://www.{host}{uri} 308
43+
}
44+
45+
http:// {
46+
import defaulthdr
47+
redir https://{env.CADDY_HOST} 308
48+
}
49+
50+
{env.CADDY_TAILNET_HOST} {
51+
import defaulthdr
52+
skip_log /health
53+
handle /health {
54+
respond "{\"status\":\"up\"}"
55+
}
56+
handle_errors {
57+
respond "{uri}: {http.error.status_code} {http.error.status_text}" {http.error.status_code}
58+
}
59+
}

caddy/data/web/robots/norobots.txt

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
User-agent: *
2+
Disallow: /

caddy/data/web/robots/robots.txt

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
User-agent: InfoTigerBot
2+
Disallow: /
3+
4+
User-agent: MJ12bot
5+
Disallow: /
6+
7+
User-agent: AhrefsBot
8+
Disallow: /
9+
10+
User-agent: Zoominfobot
11+
Disallow: /
12+
13+
User-agent: dotbot
14+
Disallow: /

caddy/docker-compose.yml

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
version: "3"
2+
services:
3+
caddy:
4+
container_name: caddy
5+
restart: always
6+
image: jumager/caddy
7+
volumes:
8+
- /var/run/docker.sock:/var/run/docker.sock
9+
- /var/run/tailscale/tailscaled.sock:/var/run/tailscale/tailscaled.sock
10+
- /run/containers:/run/containers
11+
- ./data:/data
12+
- ./config:/config
13+
ports:
14+
- 80:80
15+
- 443:443
16+
- 443:443/udp
17+
- 127.0.0.1:2019:2019
18+
environment:
19+
- CADDY_INGRESS_NETWORKS=caddy
20+
- CADDY_DOCKER_CADDYFILE_PATH=/config/Caddyfile
21+
- CADDY_HOST=server.example.org
22+
- CADDY_TAILNET_HOST=server.tailXXXXX.ts.net
23+
- CADDY_REDIS_HOST=redis-host.tailXXXXX.ts.net
24+
networks:
25+
default:
26+
name: caddy
27+
external: true

mariadb/docker-compose.yml

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
version: "3"
2+
name: mariadb
3+
services:
4+
mariadb:
5+
container_name: mariadb
6+
restart: always
7+
image: mariadb:latest
8+
volumes:
9+
- /run/mysqld:/run/mysqld
10+
- ./data:/var/lib/mysql
11+
ports:
12+
- 127.0.0.1:3306:3306
13+
- 100.X.X.X:3306:3306
14+
environment:
15+
MARIADB_ROOT_PASSWORD: Secret_password
16+
networks:
17+
default:
18+
name: caddy
19+
external: true

mariadb/dumpdb.sh

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
docker exec mariadb sh -c 'exec mysqldump --all-databases -uroot -pSecret_password' >dbdump.sql

mariadb/mysql.sh

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#!/bin/sh
2+
if test -t 0
3+
then
4+
ARGS=-t
5+
fi
6+
docker exec -i $ARGS mariadb sh -c "exec mysql -uroot -pXC6VrIjEnkl8NA80 $*"

mariadb/restoredb.sh

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
docker exec -i mariadb sh -c 'exec mysql -uroot -pXC6VrIjEnkl8NA80' <dbdump.sql

networks.sh

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
#!/bin/sh
2+
docker network create caddy

postgres/createdb.sh

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#!/bin/sh
2+
if test -t 0
3+
then
4+
ARGS=-t
5+
fi
6+
docker exec -i $ARGS postgres sh -c "exec createdb -U postgres $*"

postgres/createuser.sh

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#!/bin/sh
2+
if test -t 0
3+
then
4+
ARGS=-t
5+
fi
6+
docker exec -i $ARGS postgres sh -c "exec createuser -U postgres $*"

postgres/docker-compose.yml

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
version: "3"
2+
name: postgres
3+
services:
4+
postgres:
5+
container_name: postgres
6+
restart: always
7+
image: postgres:15-alpine
8+
volumes:
9+
- /run/postgresql:/run/postgresql
10+
- ./data:/var/lib/postgresql/data
11+
ports:
12+
- 127.0.0.1:5432:5432
13+
- 100.X.X.X:6379:5432
14+
environment:
15+
POSTGRES_PASSWORD: Secret_password
16+
networks:
17+
default:
18+
name: caddy
19+
external: true

postgres/dumpdb.sh

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
docker exec postgres sh -c 'exec pg_dumpall -U postgres -c ' >dbdump.psqldump

0 commit comments

Comments
 (0)