Skip to content

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.

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 --debug

Output 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:

TagExampleMoves when
The release tagv1.1.0Never
The version1.1.0Never
The minor line1.1Each patch release in that line
latestlatestEach 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.

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

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

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.yaml
/etc/s3mirror.yaml
destination:
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"

Start the container from the host’s scheduler with --rm, the same way Scheduling starts the installed command. In a systemd unit:

/etc/systemd/system/s3mirror.service
[Unit]
Description=Mirror S3 buckets with s3mirror
Wants=network-online.target
After=network-online.target docker.service
Requires=docker.service
[Service]
Type=oneshot
ExecStart=/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.log

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

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.