working container config
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
.git
|
||||
.github
|
||||
.vscode
|
||||
node_modules
|
||||
dist
|
||||
.astro
|
||||
coverage
|
||||
playwright-report
|
||||
test-results
|
||||
e2e
|
||||
**/*.log
|
||||
.env
|
||||
.env.*
|
||||
README.md
|
||||
@@ -0,0 +1,92 @@
|
||||
# Container deployment
|
||||
|
||||
This stack runs the Astro blog behind a Caddy reverse proxy with the
|
||||
[Coraza](https://coraza.io/) WAF (loaded with the OWASP Core Rule Set) and the
|
||||
Google Cloud DNS plugin for ACME DNS-01 certificates.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
.
|
||||
├── Containerfile # App image (node:24-trixie-slim → astro preview)
|
||||
├── caddy/
|
||||
│ ├── Containerfile # xcaddy build: Coraza + googleclouddns
|
||||
│ ├── Caddyfile # Reverse proxy + WAF + TLS config
|
||||
│ └── coraza.conf # Local Coraza overrides
|
||||
├── compose.yaml # podman compose / docker compose entrypoint
|
||||
└── quadlet/ # Systemd Quadlet units (production)
|
||||
├── dev-blog.network
|
||||
├── dev-blog-app.container
|
||||
├── dev-blog-caddy.container
|
||||
├── caddy-data.volume
|
||||
└── caddy-config.volume
|
||||
```
|
||||
|
||||
## Quick start (compose)
|
||||
|
||||
`compose.override.yaml` is auto-loaded by `podman compose`, so the default
|
||||
invocation is **dev mode** (high ports, rootless-friendly, self-signed HTTPS):
|
||||
|
||||
```sh
|
||||
podman compose up --build
|
||||
# → http://localhost:8080
|
||||
# → https://localhost:8443 (self-signed via Caddy `tls internal`)
|
||||
```
|
||||
|
||||
The dev override bind-mounts `caddy/Caddyfile.dev` into the Caddy container
|
||||
which uses `tls internal` instead of ACME, so HTTPS works locally without
|
||||
needing a real domain or GCP credentials. Your browser will warn about the
|
||||
self-signed cert; trust it for `localhost` if you want a clean page.
|
||||
|
||||
For **production** (privileged ports 80/443, ACME via Cloud DNS) skip the
|
||||
override file with an explicit `-f`:
|
||||
|
||||
```sh
|
||||
# Provide a Google Cloud service-account key with Cloud DNS admin on your zone:
|
||||
mkdir -p secrets && cp /path/to/key.json secrets/gcp-dns.json
|
||||
|
||||
# Override the public hostname / project:
|
||||
export SITE_ADDRESS=https://blog.example.com
|
||||
export ACME_EMAIL=you@example.com
|
||||
export GCP_PROJECT=my-gcp-project
|
||||
|
||||
podman compose -f compose.yaml up -d --build
|
||||
```
|
||||
|
||||
Either mode also accepts ad-hoc port overrides via `HTTP_PORT`/`HTTPS_PORT`
|
||||
environment variables.
|
||||
|
||||
## Production (Quadlet)
|
||||
|
||||
Copy the unit files into a Quadlet search path and reload systemd:
|
||||
|
||||
```sh
|
||||
# rootful
|
||||
sudo cp quadlet/* /etc/containers/systemd/
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl start dev-blog-caddy.service # pulls in app + network
|
||||
|
||||
# rootless
|
||||
mkdir -p ~/.config/containers/systemd
|
||||
cp quadlet/* ~/.config/containers/systemd/
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user start dev-blog-caddy.service
|
||||
```
|
||||
|
||||
Build the images first so the Quadlet units can find them locally:
|
||||
|
||||
```sh
|
||||
podman build -t localhost/dev-blog-app:latest .
|
||||
podman build -t localhost/dev-blog-caddy:latest ./caddy
|
||||
podman secret create gcp-dns-sa /path/to/key.json
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- The app container is *not* published to the host – Caddy reaches it on the
|
||||
internal `dev-blog` network at `app:4321`.
|
||||
- The OWASP CRS (v4.7.0 by default) is baked into the Caddy image; bump
|
||||
`CRS_VERSION` in `caddy/Containerfile` to upgrade.
|
||||
- Coraza's `load_owasp_crs` directive in the Caddyfile enables the CRS rules
|
||||
that cover the OWASP Top 10 (injection, XSS, RCE, LFI/RFI, scanner detection,
|
||||
protocol violations, session fixation, etc.).
|
||||
@@ -0,0 +1,50 @@
|
||||
# syntax=docker/dockerfile:1.7
|
||||
|
||||
# ---------- Build stage ----------
|
||||
FROM node:24-trixie-slim AS build
|
||||
|
||||
ENV PNPM_HOME=/pnpm \
|
||||
PATH=/pnpm:$PATH \
|
||||
CI=1
|
||||
|
||||
RUN corepack enable
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store \
|
||||
pnpm install --frozen-lockfile
|
||||
|
||||
COPY . .
|
||||
RUN pnpm run build
|
||||
|
||||
|
||||
# ---------- Runtime stage ----------
|
||||
FROM node:24-trixie-slim AS runtime
|
||||
|
||||
ENV NODE_ENV=production \
|
||||
HOST=0.0.0.0 \
|
||||
PORT=4321 \
|
||||
ASTRO_TELEMETRY_DISABLED=1
|
||||
|
||||
RUN groupadd --system --gid 1001 astro \
|
||||
&& useradd --system --uid 1001 --gid astro --shell /usr/sbin/nologin astro
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Only what's needed to run `astro preview`
|
||||
COPY --from=build --chown=astro:astro /app/package.json ./package.json
|
||||
COPY --from=build --chown=astro:astro /app/node_modules ./node_modules
|
||||
COPY --from=build --chown=astro:astro /app/dist ./dist
|
||||
COPY --from=build --chown=astro:astro /app/astro.config.mjs ./astro.config.mjs
|
||||
|
||||
USER astro
|
||||
|
||||
EXPOSE 4321
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
|
||||
CMD node -e "fetch('http://127.0.0.1:'+ (process.env.PORT||4321) +'/').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
|
||||
|
||||
# Invoke astro directly via node to avoid corepack/pnpm shims at runtime
|
||||
# (the rootfs is read-only and corepack would try to write a cache dir).
|
||||
CMD ["node", "./node_modules/astro/bin/astro.mjs", "preview", "--host", "0.0.0.0", "--port", "4321"]
|
||||
@@ -0,0 +1,70 @@
|
||||
# ----------------------------------------------------------------------------
|
||||
# Caddyfile – fronts the Astro app, terminates TLS, and runs the Coraza WAF
|
||||
# with the OWASP Core Rule Set loaded.
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
{
|
||||
# Make sure the WAF runs before the reverse-proxy handler.
|
||||
order coraza_waf before reverse_proxy
|
||||
|
||||
# Email used for Let's Encrypt account registration.
|
||||
email {$ACME_EMAIL:admin@example.com}
|
||||
}
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Public site
|
||||
# ----------------------------------------------------------------------------
|
||||
{$SITE_ADDRESS:http://:80} {
|
||||
encode zstd gzip
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# WAF – Coraza + OWASP Core Rule Set (Top 10 protections)
|
||||
# ------------------------------------------------------------------
|
||||
coraza_waf {
|
||||
load_owasp_crs
|
||||
|
||||
directives `
|
||||
Include @coraza.conf-recommended
|
||||
Include @crs-setup.conf.example
|
||||
Include @owasp_crs/*.conf
|
||||
SecRuleEngine On
|
||||
SecRequestBodyAccess On
|
||||
SecResponseBodyAccess Off
|
||||
SecDefaultAction "phase:1,log,auditlog,deny,status:403"
|
||||
SecDefaultAction "phase:2,log,auditlog,deny,status:403"
|
||||
`
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# TLS via ACME DNS-01 with the Google Cloud DNS provider.
|
||||
# Falls back to no-TLS automatically when SITE_ADDRESS is http://...
|
||||
# ------------------------------------------------------------------
|
||||
tls {
|
||||
dns googleclouddns {
|
||||
gcp_project {$GCP_PROJECT}
|
||||
}
|
||||
resolvers 8.8.8.8 1.1.1.1
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Reverse-proxy to the Astro container on the internal network
|
||||
# ------------------------------------------------------------------
|
||||
reverse_proxy app:4321 {
|
||||
header_up X-Real-IP {remote_host}
|
||||
header_up X-Forwarded-Proto {scheme}
|
||||
}
|
||||
|
||||
# Standard hardening headers
|
||||
header {
|
||||
Strict-Transport-Security "max-age=31536000; includeSubDomains"
|
||||
X-Content-Type-Options "nosniff"
|
||||
X-Frame-Options "SAMEORIGIN"
|
||||
Referrer-Policy "strict-origin-when-cross-origin"
|
||||
-Server
|
||||
}
|
||||
|
||||
log {
|
||||
output stdout
|
||||
format console
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
# Development Caddyfile – local HTTPS via `tls internal` (self-signed, no ACME).
|
||||
# Mounted into the Caddy container by compose.override.yaml, replacing the
|
||||
# production Caddyfile. Same WAF + reverse-proxy behaviour, just a simpler TLS
|
||||
# story so https://localhost:8443 works without any cloud credentials.
|
||||
{
|
||||
order coraza_waf before reverse_proxy
|
||||
# Disable HTTP→HTTPS auto-redirects in dev (we publish on different ports).
|
||||
auto_https disable_redirects
|
||||
}
|
||||
|
||||
# Shared handler chain for both the HTTP and HTTPS listeners.
|
||||
(site) {
|
||||
encode zstd gzip
|
||||
|
||||
coraza_waf {
|
||||
load_owasp_crs
|
||||
|
||||
directives `
|
||||
Include @coraza.conf-recommended
|
||||
Include @crs-setup.conf.example
|
||||
Include @owasp_crs/*.conf
|
||||
SecRuleEngine On
|
||||
SecRequestBodyAccess On
|
||||
SecResponseBodyAccess Off
|
||||
SecDefaultAction "phase:1,log,auditlog,deny,status:403"
|
||||
SecDefaultAction "phase:2,log,auditlog,deny,status:403"
|
||||
`
|
||||
}
|
||||
|
||||
reverse_proxy app:4321 {
|
||||
header_up X-Real-IP {remote_host}
|
||||
}
|
||||
|
||||
header {
|
||||
X-Content-Type-Options "nosniff"
|
||||
X-Frame-Options "SAMEORIGIN"
|
||||
Referrer-Policy "strict-origin-when-cross-origin"
|
||||
-Server
|
||||
}
|
||||
|
||||
log {
|
||||
output stdout
|
||||
format console
|
||||
}
|
||||
}
|
||||
|
||||
# Plain-HTTP listener – the `http://` scheme is needed so Caddy binds :80
|
||||
# instead of upgrading these names to HTTPS-only.
|
||||
http://localhost, http://127.0.0.1, http://[::1] {
|
||||
import site
|
||||
}
|
||||
|
||||
# HTTPS listener with a self-signed cert from Caddy's local CA.
|
||||
https://localhost, https://127.0.0.1, https://[::1] {
|
||||
tls internal
|
||||
import site
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
# syntax=docker/dockerfile:1.7
|
||||
|
||||
# Build a custom Caddy with the Coraza WAF plugin and the
|
||||
# Google Cloud DNS provider for ACME DNS-01 challenges.
|
||||
|
||||
ARG CADDY_VERSION=2.11.2
|
||||
|
||||
FROM caddy:${CADDY_VERSION}-builder AS builder
|
||||
|
||||
# coraza-caddy/v2 requires Go >= 1.25, but the caddy:builder image still ships
|
||||
# Go 1.24. GOTOOLCHAIN=auto lets the Go toolchain transparently download the
|
||||
# version requested by each module's go.mod.
|
||||
ENV GOTOOLCHAIN=auto
|
||||
|
||||
RUN xcaddy build \
|
||||
--with github.com/corazawaf/coraza-caddy/v2 \
|
||||
--with github.com/caddy-dns/googleclouddns
|
||||
|
||||
|
||||
FROM caddy:${CADDY_VERSION}
|
||||
|
||||
# OWASP Core Rule Set (CRS) – pinned, baked into the image so it's available offline.
|
||||
ARG CRS_VERSION=4.7.0
|
||||
RUN set -eux; \
|
||||
apk add --no-cache --virtual .fetch curl tar; \
|
||||
mkdir -p /etc/caddy/coraza /etc/caddy/coraza/owasp_crs; \
|
||||
curl -fsSL "https://github.com/coreruleset/coreruleset/archive/refs/tags/v${CRS_VERSION}.tar.gz" \
|
||||
-o /tmp/crs.tgz; \
|
||||
tar -xzf /tmp/crs.tgz -C /tmp; \
|
||||
cp -r "/tmp/coreruleset-${CRS_VERSION}/rules" /etc/caddy/coraza/owasp_crs/rules; \
|
||||
cp "/tmp/coreruleset-${CRS_VERSION}/crs-setup.conf.example" /etc/caddy/coraza/crs-setup.conf; \
|
||||
curl -fsSL https://raw.githubusercontent.com/corazawaf/coraza/main/coraza.conf-recommended \
|
||||
-o /etc/caddy/coraza/coraza.conf; \
|
||||
rm -rf /tmp/crs.tgz "/tmp/coreruleset-${CRS_VERSION}"; \
|
||||
apk del .fetch
|
||||
|
||||
COPY --from=builder /usr/bin/caddy /usr/bin/caddy
|
||||
COPY Caddyfile /etc/caddy/Caddyfile
|
||||
COPY coraza.conf /etc/caddy/coraza/local.conf
|
||||
@@ -0,0 +1,23 @@
|
||||
# ---------------------------------------------------------------------------
|
||||
# Local Coraza overrides.
|
||||
#
|
||||
# The recommended base config and the OWASP CRS are loaded from the Caddyfile
|
||||
# via the `load_owasp_crs` directive (which exposes them under the
|
||||
# @coraza.conf-recommended, @crs-setup.conf.example, and @owasp_crs/* aliases).
|
||||
#
|
||||
# Add per-site exceptions / tuning below.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Engine in blocking mode.
|
||||
SecRuleEngine On
|
||||
|
||||
# Reasonable request-body limits for a static blog.
|
||||
SecRequestBodyLimit 13107200
|
||||
SecRequestBodyNoFilesLimit 131072
|
||||
SecRequestBodyLimitAction Reject
|
||||
|
||||
# Drop very noisy false-positives on static asset paths.
|
||||
SecRule REQUEST_URI "@beginsWith /_astro/" \
|
||||
"id:1000,phase:1,pass,nolog,ctl:ruleEngine=Off"
|
||||
SecRule REQUEST_URI "@beginsWith /fonts/" \
|
||||
"id:1001,phase:1,pass,nolog,ctl:ruleEngine=Off"
|
||||
@@ -0,0 +1,50 @@
|
||||
# Development override – auto-loaded by `podman compose` / `docker compose`
|
||||
# when present alongside compose.yaml. Maps Caddy to unprivileged host ports
|
||||
# so it works under rootless Podman without tweaking
|
||||
# net.ipv4.ip_unprivileged_port_start.
|
||||
#
|
||||
# It also pins static IPs and adds a /etc/hosts override on the Caddy
|
||||
# container to work around aardvark-dns leaving stale records around between
|
||||
# rootless `podman compose down` / `up` cycles. Production (Quadlets / `-f
|
||||
# compose.yaml`) keeps relying on standard container DNS.
|
||||
#
|
||||
# Default usage (dev, high ports, plain HTTP):
|
||||
# podman compose up --build
|
||||
# → http://localhost:8080
|
||||
#
|
||||
# To skip this override and run with prod (low) ports:
|
||||
# podman compose -f compose.yaml up --build
|
||||
|
||||
networks:
|
||||
dev-blog:
|
||||
ipam:
|
||||
config:
|
||||
- subnet: 10.89.99.0/24
|
||||
|
||||
services:
|
||||
app:
|
||||
networks: !override
|
||||
dev-blog:
|
||||
ipv4_address: 10.89.99.10
|
||||
|
||||
caddy:
|
||||
environment:
|
||||
# In dev we drive listeners from the Caddyfile itself (see below), so
|
||||
# SITE_ADDRESS is unused but kept harmless.
|
||||
SITE_ADDRESS: ${SITE_ADDRESS:-:80}
|
||||
networks: !override
|
||||
dev-blog:
|
||||
ipv4_address: 10.89.99.11
|
||||
# Pin `app` to the static IP above so we never depend on aardvark-dns,
|
||||
# which is prone to caching stale entries between rootless restarts.
|
||||
extra_hosts:
|
||||
- "app:10.89.99.10"
|
||||
# Swap the production Caddyfile for one that uses `tls internal` so
|
||||
# https://localhost:8443 works without ACME / Cloud DNS credentials.
|
||||
volumes:
|
||||
- ./caddy/Caddyfile.dev:/etc/caddy/Caddyfile:ro
|
||||
# !override replaces the ports list from compose.yaml instead of appending.
|
||||
ports: !override
|
||||
- "${HTTP_PORT:-8080}:80"
|
||||
- "${HTTPS_PORT:-8443}:443"
|
||||
- "${HTTPS_PORT:-8443}:443/udp"
|
||||
@@ -0,0 +1,84 @@
|
||||
# Podman-compose / Docker-compose file for the dev-blog stack.
|
||||
#
|
||||
# This is provided as a convenience for local iteration; production deployments
|
||||
# should use the Quadlet units in ./quadlet/ which integrate with systemd.
|
||||
#
|
||||
# Usage:
|
||||
# podman compose up --build
|
||||
#
|
||||
# The Caddy container needs a Google Cloud service-account JSON key mounted at
|
||||
# /run/secrets/gcp-dns.json for the ACME DNS-01 challenge to work. For local
|
||||
# HTTP-only development, set SITE_ADDRESS=http://:80 in your shell or .env.
|
||||
|
||||
name: dev-blog
|
||||
|
||||
networks:
|
||||
dev-blog:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
caddy-data:
|
||||
caddy-config:
|
||||
|
||||
services:
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Containerfile
|
||||
image: localhost/dev-blog-app:latest
|
||||
container_name: dev-blog-app
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
HOST: 0.0.0.0
|
||||
PORT: "4321"
|
||||
ASTRO_TELEMETRY_DISABLED: "1"
|
||||
networks:
|
||||
- dev-blog
|
||||
expose:
|
||||
- "4321"
|
||||
read_only: true
|
||||
tmpfs:
|
||||
- /tmp:size=64m,mode=1777
|
||||
cap_drop:
|
||||
- ALL
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
|
||||
caddy:
|
||||
build:
|
||||
context: ./caddy
|
||||
dockerfile: Containerfile
|
||||
image: localhost/dev-blog-caddy:latest
|
||||
container_name: dev-blog-caddy
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- app
|
||||
environment:
|
||||
SITE_ADDRESS: ${SITE_ADDRESS:-https://example.com}
|
||||
ACME_EMAIL: ${ACME_EMAIL:-admin@example.com}
|
||||
GCP_PROJECT: ${GCP_PROJECT:-my-gcp-project}
|
||||
GOOGLE_APPLICATION_CREDENTIALS: /run/secrets/gcp-dns.json
|
||||
secrets:
|
||||
- gcp-dns
|
||||
networks:
|
||||
- dev-blog
|
||||
ports:
|
||||
- "${HTTP_PORT:-80}:80"
|
||||
- "${HTTPS_PORT:-443}:443"
|
||||
- "${HTTPS_PORT:-443}:443/udp"
|
||||
volumes:
|
||||
- caddy-data:/data
|
||||
- caddy-config:/config
|
||||
cap_drop:
|
||||
- ALL
|
||||
cap_add:
|
||||
- NET_BIND_SERVICE
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
|
||||
secrets:
|
||||
gcp-dns:
|
||||
# Path to a Google Cloud service-account JSON key with permission to
|
||||
# update Cloud DNS records for the SITE_ADDRESS zone.
|
||||
file: ./secrets/gcp-dns.json
|
||||
@@ -0,0 +1,8 @@
|
||||
[Unit]
|
||||
Description=Persistent Caddy config cache
|
||||
|
||||
[Volume]
|
||||
VolumeName=caddy-config
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target default.target
|
||||
@@ -0,0 +1,8 @@
|
||||
[Unit]
|
||||
Description=Persistent Caddy data (certificates, ACME state)
|
||||
|
||||
[Volume]
|
||||
VolumeName=caddy-data
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target default.target
|
||||
@@ -0,0 +1,34 @@
|
||||
[Unit]
|
||||
Description=dev-blog Astro application (node:24-trixie-slim)
|
||||
Wants=network-online.target
|
||||
After=network-online.target
|
||||
|
||||
[Container]
|
||||
ContainerName=dev-blog-app
|
||||
# Built from the Containerfile at the repo root.
|
||||
# Build with: podman build -t localhost/dev-blog-app:latest .
|
||||
Image=localhost/dev-blog-app:latest
|
||||
|
||||
Network=dev-blog.network
|
||||
NetworkAlias=app
|
||||
|
||||
Environment=NODE_ENV=production
|
||||
Environment=HOST=0.0.0.0
|
||||
Environment=PORT=4321
|
||||
Environment=ASTRO_TELEMETRY_DISABLED=1
|
||||
|
||||
# Hardening
|
||||
NoNewPrivileges=true
|
||||
ReadOnly=true
|
||||
DropCapability=ALL
|
||||
Tmpfs=/tmp:rw,size=64m,mode=1777
|
||||
|
||||
# Not exposed publicly – Caddy reverse-proxies in over the internal network.
|
||||
# PublishPort=4321:4321
|
||||
|
||||
[Service]
|
||||
Restart=on-failure
|
||||
TimeoutStartSec=120
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target default.target
|
||||
@@ -0,0 +1,46 @@
|
||||
[Unit]
|
||||
Description=Caddy reverse proxy with Coraza WAF (OWASP CRS) and Google Cloud DNS plugin
|
||||
Wants=network-online.target
|
||||
After=network-online.target dev-blog-app.service
|
||||
Requires=dev-blog-app.service
|
||||
|
||||
[Container]
|
||||
ContainerName=dev-blog-caddy
|
||||
# Build with: podman build -t localhost/dev-blog-caddy:latest ./caddy
|
||||
Image=localhost/dev-blog-caddy:latest
|
||||
|
||||
Network=dev-blog.network
|
||||
NetworkAlias=caddy
|
||||
|
||||
PublishPort=80:80
|
||||
PublishPort=443:443
|
||||
PublishPort=443:443/udp
|
||||
|
||||
# --- Configuration ---
|
||||
Environment=SITE_ADDRESS=https://example.com
|
||||
Environment=ACME_EMAIL=admin@example.com
|
||||
# Required by the googleclouddns plugin; must match a GCP project that owns
|
||||
# the DNS zone for SITE_ADDRESS.
|
||||
Environment=GCP_PROJECT=my-gcp-project
|
||||
# A Workload-Identity / service-account JSON key mounted read-only below.
|
||||
Environment=GOOGLE_APPLICATION_CREDENTIALS=/run/secrets/gcp-dns.json
|
||||
|
||||
# Mount the GCP service-account key as a read-only secret.
|
||||
# Create with: podman secret create gcp-dns-sa /path/to/key.json
|
||||
Secret=gcp-dns-sa,type=mount,target=gcp-dns.json,mode=0400
|
||||
|
||||
# Persistent state for ACME certificates and Caddy's data directory.
|
||||
Volume=caddy-data.volume:/data
|
||||
Volume=caddy-config.volume:/config
|
||||
|
||||
# Hardening
|
||||
NoNewPrivileges=true
|
||||
DropCapability=ALL
|
||||
AddCapability=CAP_NET_BIND_SERVICE
|
||||
|
||||
[Service]
|
||||
Restart=on-failure
|
||||
TimeoutStartSec=120
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target default.target
|
||||
@@ -0,0 +1,10 @@
|
||||
[Unit]
|
||||
Description=Internal network for the dev-blog stack
|
||||
|
||||
[Network]
|
||||
NetworkName=dev-blog
|
||||
Driver=bridge
|
||||
DisableDNS=false
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target default.target
|
||||
Reference in New Issue
Block a user