GuidesContainer
Container
Run the GHCR image with a mounted config, keep a log on the host, pin a version, and schedule it.
The image is ghcr.io/netspeedy/s3mirror, built for linux/amd64 and linux/arm64 from the
repository’s Dockerfile: a slim Python image with the three packages installed and s3mirror.py
copied in. Its entrypoint is s3mirror, so everything after the image name is passed to s3mirror as
flags. With no flags it prints its help.
Run it
Section titled “Run it”Mount the config file read-only and point --config at it:
docker run --rm \ -v "$PWD/config.yaml:/config/config.yaml:ro" \ ghcr.io/netspeedy/s3mirror:v1.1.0 \ --config /config/config.yaml --no-delete --debugOutput goes to the container’s standard output unbuffered, so docker logs and CI logs show it as
it happens, and docker run exits with s3mirror’s exit code.
The container needs to reach both endpoints. If an endpoint’s hostname only resolves on the host,
or it’s on localhost, run with --network host or use an address the container can reach.
Every stable release is published under four tags:
| Tag | Example | Moves when |
|---|---|---|
| The release tag | v1.1.0 | Never |
| The version | 1.1.0 | Never |
| The minor line | 1.1 | Each patch release in that line |
latest | latest | Each release |
Pin the release tag or the version for scheduled jobs, so an upgrade happens when you choose. Images
are published only for stable tags on main, after a check that the tag matches the script’s
__version__, and carry build provenance and an SBOM.
Keeping a log on the host
Section titled “Keeping a log on the host”Mount a directory for the log and pass --log-file inside it:
docker run --rm \ --user "$(id -u):$(id -g)" \ -v /etc/s3mirror.yaml:/config/config.yaml:ro \ -v /var/log/s3mirror:/logs \ ghcr.io/netspeedy/s3mirror:v1.1.0 \ --config /config/config.yaml --log-file /logs/s3mirror.logThe image runs as an unprivileged s3mirror user created at build time, with no fixed UID, so it
usually can’t write to a host directory owned by someone else. --user runs it as your own UID and
GID instead, which can write wherever you can; the script and its packages are readable by any
user. If the log can’t be opened, s3mirror stops with a traceback and exit code 1 before connecting
to anything.
The config only has to be readable by the user the container runs as. With --user, a config that’s
chmod 600 and owned by you works.
A private CA
Section titled “A private CA”If an endpoint uses a certificate from your own CA, mount the CA bundle and give its path inside the
container as verify_ssl:
docker run --rm \ -v /etc/s3mirror.yaml:/config/config.yaml:ro \ -v /etc/ssl/certs/internal-ca.pem:/config/internal-ca.pem:ro \ ghcr.io/netspeedy/s3mirror:v1.1.0 \ --config /config/config.yamldestination: endpoint_url: "https://minio.internal.example:9000" aws_access_key_id: "DEST_ACCESS_KEY" aws_secret_access_key: "DEST_SECRET_KEY" verify_ssl: "/config/internal-ca.pem"Scheduling
Section titled “Scheduling”Start the container from the host’s scheduler with --rm, the same way
Scheduling starts the installed command. In a systemd unit:
[Unit]Description=Mirror S3 buckets with s3mirrorWants=network-online.targetAfter=network-online.target docker.serviceRequires=docker.service
[Service]Type=oneshotExecStart=/usr/bin/docker run --rm --init --name s3mirror \ --user 1000:1000 \ -v /etc/s3mirror.yaml:/config/config.yaml:ro \ -v /var/log/s3mirror:/logs \ ghcr.io/netspeedy/s3mirror:v1.1.0 \ --config /config/config.yaml --log-file /logs/s3mirror.logPair it with the timer on the Scheduling page. Use the UID and GID that own /var/log/s3mirror in
--user. The fixed --name makes a second container refuse to start while one is still running.
Stopping it
Section titled “Stopping it”docker stop sends SIGTERM, which s3mirror handles even as process 1 in the container. It drops
the copies or deletions still queued for the current bucket, waits for the ones already running,
logs Operation stopped by SIGTERM and exits 143 without a summary. The next run picks up where it
left off.
docker stop only waits 10 seconds by default before it kills the container, which then exits 137,
and a large object in flight can take longer than that. Give it more time with docker stop -t 300,
or --stop-timeout 300 on docker run. The --init in the unit above isn’t needed for any of this,
but does no harm.
Ctrl+C on an attached docker run is passed on as SIGINT, which stops it the same way and exits
130. A second Ctrl+C, or a second SIGTERM, stops at once without waiting for the transfers in
progress.