podman compose nginx
单机容器化方案,在debian13上,用podman实现docker compose。
web网关用nginx容器,自动acme管理证书,通过pasta网络反向代理到其它后端容器。
安装debian13后设置
SSH Server设置
不允许密码方式登录,允许Root登录。
# By default password auth is allowed, we want to disable it and only allow ssh keys
echo openssh-server openssh-server/password-authentication boolean false | debconf-set-selections
# In this case true = 'PermitRootLogin prohibit-password'; false = 'PermitRootLogin yes'
echo openssh-server oopenssh-server/permit-root-login boolean true | debconf-set-selections
# Delete existing config file so that `/var/lib/dpkg/info/openssh-server.config`
# does not reset `debconf` options when we do dpkg-reconfigure
rm /etc/ssh/sshd_config
# Generate stock sshd_config file
# Script `/var/lib/dpkg/info/openssh-server.postinst` uses ufc(1) to provide
# user with a dialog for 3 way merge in case of local modifications
# We want to use the stock config file (CONFNEW) and also create it as it's missing (CONFMISS)
UCF_FORCE_CONFFMISS=1 UCF_FORCE_CONFFNEW=1 dpkg-reconfigure openssh-server启用netplan
参考
https://pedroagrodrigues.com/posts/Debian_To_Netplan/
sudo apt install netplan.io systemd-resolved tmux
sudo systemctl unmask systemd-networkd.service
sudo systemctl unmask systemd-resolved.service
sudo systemctl enable --now systemd-networkd.service
sudo systemctl mask networking
sudo ln -sf /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf
sudo systemctl enable --now systemd-resolved.service
#apply、try都包含执行generate,try会120秒后回滚apply不会
sudo ENABLE_TEST_COMMANDS=1 netplan migrate
sudo chmod 600 /etc/netplan/*
#用netplan方式设置网络
sudo nano /etc/netplan/10-ifupdown.yaml
#在ssh下可能会丢失连接
tmux
sudo netplan try
#如ssh连接丢失,关闭旧终端窗口,
#重新打开新终端窗口,重新ssh登录,
#tmux a
#回车确认netplan try
sudo netplan generate
sudo apt purge ifupdown resolvconf
sudo rm -rf /etc/network
ping google.com禁用systemd-resolved的LLMNR和MulticastDNS
llmnr会监听tcp udp 5355端口。
multicastdns会监听udp 5353端口。
#systemd-analyze cat-config systemd/resolved.conf
sudo mkdir -p /etc/systemd/resolved.conf.d/
sudo nano /etc/systemd/resolved.conf.d/60-disable-llmnr.conf[Resolve]
LLMNR=no
MulticastDNS=no
安装rootless podman
sudo apt install podman
podman run quay.io/podman/hello
sudo loginctl enable-linger $(id -un)
#docker compose容器在开机/关机时自动启停依赖该服务,原理见下面的podman-restart.service说明
systemctl --user enable --now podman-restart.service
#启用podman
systemctl --user enable --now podman.socket
#systemctl --user status podman.socket
nano ~/.profile在最后一行添加,内容为
export DOCKER_HOST=unix:///run/user/$UID/podman/podman.socksudo nano /etc/sysctl.d/privileged_ports.conf添加非特权起始端口
net.ipv4.ip_unprivileged_port_start=0不必重启,重设sysctl
sudo systemctl restart systemd-sysctlsudo apt install docker-compose默认仓库
Docker Hub (docker.io)、
Red Hat Quay (quay.io)、
GitHub Container Registry (ghcr.io)、
Google Container Registry (gcr.io)。
debian13 trixie 对应podman版本v5.4.2,
参考文档
https://docs.podman.io/en/v5.4.2/markdown/podman-systemd.unit.5.html
podman-restart.service说明
系统启动时会调用ExecStart,系统关闭时会调用ExecStop。
ExecStart时会调用podman start,启动所有restart-policy=always的容器;
ExecStop时会调用podman stop,停止所有restart-policy=always的容器。
quadlet
如不使用quadlet可忽略此部分。
https://docs.podman.io/en/stable/markdown/podman-systemd.unit.5.html
quadlet依赖用户服务podman-user-wait-network-online.service来等待网络就绪。
启用network-online.target
用户服务podman-user-wait-network-online.service会依赖系统服务network-online.target。
如无系统服务依赖network-online.target,可按下面方法手动启动。
# 创建multi-user.target的wants目录(如果不存在)
sudo mkdir -p /etc/systemd/system/multi-user.target.wants
# 创建符号链接
sudo ln -sf /lib/systemd/system/network-online.target /etc/systemd/system/multi-user.target.wants/
# 重新加载systemd配置
sudo systemctl daemon-reloadnginx
Dockerfile and Build Image
参考
https://github.com/nginx/docker-nginx/blob/1.29.4/modules/Dockerfile
https://github.com/nginx/docker-nginx/blob/master/modules/Dockerfile
在第61、62行之间(即make module-$module前)添加
if [ "$module" = "acme" ]; then \
sed -i "/dh_clean/s/dh_clean/dh_clean --exclude='.*.orig' --exclude='*.orig'/" /pkg-oss/debian/debuild-module-acme/nginx-${NGINX_VERSION}/debian/rules; \
fi; \
#dh_clean --exclude='.*.orig' --exclude='*.orig'完全版
ARG NGINX_FROM_IMAGE=nginx:mainline
FROM ${NGINX_FROM_IMAGE} AS builder
ARG ENABLED_MODULES
SHELL ["/bin/bash", "-exo", "pipefail", "-c"]
RUN if [ "$ENABLED_MODULES" = "" ]; then \
echo "No additional modules enabled, exiting"; \
exit 1; \
fi
COPY ./ /modules/
#install Rust begin
ENV RUSTUP_HOME=/usr/local/rustup \
CARGO_HOME=/usr/local/cargo \
PATH=/usr/local/cargo/bin:$PATH \
RUST_VERSION=1.92.0
#FROM debian:bookworm
RUN set -eux; \
apt-get update; \
apt-get install -y --no-install-recommends \
ca-certificates \
curl \
gnupg \
netbase \
sq \
wget \
pkg-config \
; \
#FROM buildpack-deps:bookworm-curl
#RUN set -eux; \
# apt-get update; \
apt-get install -y --no-install-recommends \
git \
mercurial \
openssh-client \
subversion \
\
# procps is very common in build systems, and is a reasonably small package
procps \
; \
#FROM buildpack-deps:bookworm-scm
#RUN set -ex; \
# apt-get update; \
apt-get install -y --no-install-recommends \
autoconf \
automake \
bzip2 \
default-libmysqlclient-dev \
dpkg-dev \
file \
g++ \
gcc \
imagemagick \
libbz2-dev \
libc6-dev \
libcurl4-openssl-dev \
libdb-dev \
libevent-dev \
libffi-dev \
libgdbm-dev \
libglib2.0-dev \
libgmp-dev \
libjpeg-dev \
libkrb5-dev \
liblzma-dev \
libmagickcore-dev \
libmagickwand-dev \
libmaxminddb-dev \
libncurses5-dev \
libncursesw5-dev \
libpng-dev \
libpq-dev \
libreadline-dev \
libsqlite3-dev \
libssl-dev \
libtool \
libwebp-dev \
libxml2-dev \
libxslt-dev \
libyaml-dev \
make \
patch \
unzip \
xz-utils \
zlib1g-dev \
; \
#FROM buildpack-deps:bookworm
#RUN set -eux; \
\
arch="$(dpkg --print-architecture)"; \
case "$arch" in \
'amd64') \
rustArch='x86_64-unknown-linux-gnu'; \
rustupSha256='20a06e644b0d9bd2fbdbfd52d42540bdde820ea7df86e92e533c073da0cdd43c'; \
;; \
'armhf') \
rustArch='armv7-unknown-linux-gnueabihf'; \
rustupSha256='3b8daab6cc3135f2cd4b12919559e6adaee73a2fbefb830fadf0405c20231d61'; \
;; \
'arm64') \
rustArch='aarch64-unknown-linux-gnu'; \
rustupSha256='e3853c5a252fca15252d07cb23a1bdd9377a8c6f3efa01531109281ae47f841c'; \
;; \
'i386') \
rustArch='i686-unknown-linux-gnu'; \
rustupSha256='a5db2c4b29d23e9b318b955dd0337d6b52e93933608469085c924e0d05b1df1f'; \
;; \
'ppc64el') \
rustArch='powerpc64le-unknown-linux-gnu'; \
rustupSha256='acd89c42b47c93bd4266163a7b05d3f26287d5148413c0d47b2e8a7aa67c9dc0'; \
;; \
's390x') \
rustArch='s390x-unknown-linux-gnu'; \
rustupSha256='726b7fd5d8805e73eab4a024a2889f8859d5a44e36041abac0a2436a52d42572'; \
;; \
*) \
echo >&2 "unsupported architecture: $arch"; \
exit 1; \
;; \
esac; \
\
url="https://static.rust-lang.org/rustup/archive/1.28.2/${rustArch}/rustup-init"; \
wget --progress=dot:giga "$url"; \
echo "${rustupSha256} *rustup-init" | sha256sum -c -; \
\
chmod +x rustup-init; \
./rustup-init -y --no-modify-path --profile minimal --default-toolchain $RUST_VERSION --default-host ${rustArch}; \
rm rustup-init; \
chmod -R a+w $RUSTUP_HOME $CARGO_HOME; \
\
rustup --version; \
cargo --version; \
rustc --version;
#install Rust end
RUN apt-get update \
&& apt-get install -y --no-install-suggests --no-install-recommends \
patch make wget git devscripts debhelper dpkg-dev \
quilt lsb-release build-essential libxml2-utils xsltproc \
equivs git g++ libparse-recdescent-perl \
&& XSLSCRIPT_SHA512="f7194c5198daeab9b3b0c3aebf006922c7df1d345d454bd8474489ff2eb6b4bf8e2ffe442489a45d1aab80da6ecebe0097759a1e12cc26b5f0613d05b7c09ffa *stdin" \
&& wget -O /tmp/xslscript.pl https://raw.githubusercontent.com/nginx/xslscript/9204424259c343ca08a18a78915f40f28025e093/xslscript.pl \
&& if [ "$(cat /tmp/xslscript.pl | openssl sha512 -r)" = "$XSLSCRIPT_SHA512" ]; then \
echo "XSLScript checksum verification succeeded!"; \
chmod +x /tmp/xslscript.pl; \
mv /tmp/xslscript.pl /usr/local/bin/; \
else \
echo "XSLScript checksum verification failed!"; \
exit 1; \
fi \
&& git clone -b ${NGINX_VERSION}-${PKG_RELEASE%%~*} https://github.com/nginx/pkg-oss/ \
&& cd pkg-oss \
&& mkdir /tmp/packages \
&& for module in $ENABLED_MODULES; do \
echo "Building $module for nginx-$NGINX_VERSION"; \
if [ -d /modules/$module ]; then \
echo "Building $module from user-supplied sources"; \
# check if module sources file is there and not empty
if [ ! -s /modules/$module/source ]; then \
echo "No source file for $module in modules/$module/source, exiting"; \
exit 1; \
fi; \
# some modules require build dependencies
if [ -f /modules/$module/build-deps ]; then \
echo "Installing $module build dependencies"; \
apt-get update && apt-get install -y --no-install-suggests --no-install-recommends $(cat /modules/$module/build-deps | xargs); \
fi; \
# if a module has a build dependency that is not in a distro, provide a
# shell script to fetch/build/install those
# note that shared libraries produced as a result of this script will
# not be copied from the builder image to the main one so build static
if [ -x /modules/$module/prebuild ]; then \
echo "Running prebuild script for $module"; \
/modules/$module/prebuild; \
fi; \
/pkg-oss/build_module.sh -v $NGINX_VERSION -f -y -o /tmp/packages -n $module $(cat /modules/$module/source); \
BUILT_MODULES="$BUILT_MODULES $(echo $module | tr '[A-Z]' '[a-z]' | tr -d '[/_\-\.\t ]')"; \
elif make -C /pkg-oss/debian list | grep -P "^$module\s+\d" > /dev/null; then \
echo "Building $module from pkg-oss sources"; \
cd /pkg-oss/debian; \
make rules-module-$module BASE_VERSION=$NGINX_VERSION NGINX_VERSION=$NGINX_VERSION; \
mk-build-deps --install --tool="apt-get -o Debug::pkgProblemResolver=yes --no-install-recommends --yes" debuild-module-$module/nginx-$NGINX_VERSION/debian/control; \
if [ "$module" = "acme" ]; then \
sed -i "/dh_clean/s/dh_clean/dh_clean --exclude='.*.orig' --exclude='*.orig'/" /pkg-oss/debian/debuild-module-acme/nginx-${NGINX_VERSION}/debian/rules; \
fi; \
#dh_clean --exclude='.*.orig' --exclude='*.orig'
make module-$module BASE_VERSION=$NGINX_VERSION NGINX_VERSION=$NGINX_VERSION; \
find ../../ -maxdepth 1 -mindepth 1 -type f -name "*.deb" -exec mv -v {} /tmp/packages/ \;; \
BUILT_MODULES="$BUILT_MODULES $module"; \
else \
echo "Don't know how to build $module module, exiting"; \
exit 1; \
fi; \
done \
&& echo "BUILT_MODULES=\"$BUILT_MODULES\"" > /tmp/packages/modules.env
FROM ${NGINX_FROM_IMAGE}
RUN --mount=type=bind,target=/tmp/packages/,source=/tmp/packages/,from=builder \
apt-get update \
&& . /tmp/packages/modules.env \
&& for module in $BUILT_MODULES; do \
apt-get install --no-install-suggests --no-install-recommends -y /tmp/packages/nginx-module-${module}_${NGINX_VERSION}*.deb; \
done \
&& rm -rf /var/lib/apt/lists/
podman build --build-arg ENABLED_MODULES="acme brotli" --build-arg NGINX_FROM_IMAGE=docker.io/nginx:stable -t nginx-stable-with-acme-brotli .docker compose
.env
NGX_ACME_EMAIL=nobody@home.arpacompose.yaml
services:
web:
image: nginx-stable-with-acme-brotli
network_mode: "host"
#ports:
# - 80:80
# - 443:443
volumes:
#- ./html:/usr/share/nginx/html
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./site_a.home.arpa.conf.template:/etc/nginx/templates/site_a.home.arpa.conf.template:ro
- ./site_b.home.arpa.conf.template:/etc/nginx/templates/site_b.home.arpa.conf.template:ro
- ./acme:/etc/nginx/acme:rw
environment:
#各个service可以引用.env中的环境变量,作为示例参考下面的${NGX_ACME_EMAIL}
NGX_ACME_EMAIL: ${NGX_ACME_EMAIL}
NGX_DOMAINNAME_A: site_a.home.arpa
NGX_DOMAINNAME_B: site_b.home.arpa
subweb:
image: nginx-stable-with-acme-brotli
network_mode: pasta
ports:
- 127.0.0.1:8080:80
volumes:
- ./subnginx.conf:/etc/nginx/nginx.conf:ronginx.conf
#/etc/nginx/nginx.conf
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /run/nginx.pid;
#acme module
load_module modules/ngx_http_acme_module.so;
#brotli module
load_module modules/ngx_http_brotli_filter_module.so;
load_module modules/ngx_http_brotli_static_module.so;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
gzip on;
#https://nginx.org/en/docs/http/ngx_http_gzip_module.html
gzip_static on;
#https://nginx.org/en/docs/http/ngx_http_gzip_static_module.html
#brotli on;
brotli_static on;
#https://github.com/google/ngx_brotli
#acme_issuer begin
resolver 127.0.0.53 ipv6=off valid=5s;
#resolver 127.0.0.53; #fail
acme_issuer letsencrypt {
uri https://acme-v02.api.letsencrypt.org/directory;
contact ${NGX_ACME_EMAIL};
state_path /etc/nginx/acme;
accept_terms_of_service;
ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt;
}
#acme_shared_zone zone=ngx_acme_shared:256k;
server {
# listener on port 80 is required to process ACME HTTP-01 challenges
listen 80;
location / {
return 404;
}
}
#acme_issuer end
include /etc/nginx/conf.d/*.conf;
}
site_a.home.arpa.conf.template
server {
listen 443 ssl;
server_name ${NGX_DOMAINNAME_A};
ssl_certificate_cache max=2;
#acme_certificate letsencrypt key=rsa;
acme_certificate letsencrypt;
ssl_certificate $acme_certificate;
ssl_certificate_key $acme_certificate_key;
location /nginx {
proxy_pass http://127.0.0.1:8080/;
}
location / {
root /usr/share/nginx/html;
#default_type text/html;
#return 200 "<!DOCTYPE html><h2>my nginx 2025/09/25 !</h2>\n";
}
}
site_b.home.arpa.conf.template
server {
listen 443 ssl;
server_name ${NGX_DOMAINNAME_B};
ssl_certificate_cache max=2;
#acme_certificate letsencrypt key=rsa;
acme_certificate letsencrypt;
ssl_certificate $acme_certificate;
ssl_certificate_key $acme_certificate_key;
location /nginx {
proxy_pass http://127.0.0.1:8080/;
}
location / {
root /usr/share/nginx/html;
#default_type text/html;
#return 200 "<!DOCTYPE html><h2>my nginx 2025/09/25 !</h2>\n";
}
}
subnginx.conf
#/etc/nginx/nginx.conf
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /run/nginx.pid;
#brotli module
load_module modules/ngx_http_brotli_filter_module.so;
load_module modules/ngx_http_brotli_static_module.so;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
gzip on;
#https://nginx.org/en/docs/http/ngx_http_gzip_module.html
gzip_static on;
#https://nginx.org/en/docs/http/ngx_http_gzip_static_module.html
#brotli on;
brotli_static on;
#https://github.com/google/ngx_brotli
server {
# listener on port 80 is required to process ACME HTTP-01 challenges
listen 80;
location / {
root /usr/share/nginx/html;
}
}
}
#启动,并在终端输出log
docker compose up
#访问https://site_a.home.arpa/nginx
#访问https://site_b.home.arpa/nginx
#看是否正常
#如无异常,在另一终端停止
docker compose down
#然后重新在后台启动
docker compose up -d