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.sock
sudo nano /etc/sysctl.d/privileged_ports.conf

添加非特权起始端口

net.ipv4.ip_unprivileged_port_start=0

不必重启,重设sysctl

sudo systemctl restart systemd-sysctl
sudo 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-reload

nginx

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.arpa

compose.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:ro

nginx.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