The World Vol 1 · A World That Ticks
ch 11 / 105
Chapter 11

A World in a Container

Code history and identity

go run ./cmd/worldd makes the valley exist for exactly as long as that terminal stays open and your hand stays off Ctrl-C. The hosted rule is a three-way split: a world you can host keeps its code, its history and its identity in three separate places, and none of the three is a property of the machine it happens to be running on.

Close the laptop and time stops. Whatever else this is, it is not yet something you can host.

The running world borrows a Go toolchain, of a version nobody checked at startup. It borrows the source tree, because go run compiles from source every single time.

It borrows your current directory, since -log world.jsonl means whatever directory you were standing in. It borrows your attention, because the program dies with the shell that launched it.

Code goes into a container image, built once and identical everywhere it lands. History goes into a volume, which outlives every process that writes to it. Identity, which here means the seed, arrives from the environment at startup.

Hold those three apart and you can start the world with one command, walk away, and read what it did through podman logs.

Environment defaults

Identity first, because it changes the program least. Last chapter gave worldd flags: -seed, -ticks, -log, -fast. Flags are the right interface for a person at a keyboard, and they keep working here, because a container run command can pass arguments straight through to the program inside. They are the wrong interface for everything else that starts this world, though. Compose files, unit files and orchestrators all configure a container the same way: by handing it environment variables. The fix is not to choose. Let the environment set the flag defaults, and let a flag on the command line beat the environment whenever somebody bothers to type one.

▣ Build · stage 1: defaults that come from outside the binary
// cmd/worldd/main.go — above main

// defaults are the flag defaults, which the environment is allowed to
// change before the command line gets its turn.
type defaults struct {
	seed   uint64
	ticks  uint64
	report uint64
	log    string
}

// envNum reads an unsigned number from the environment, or returns
// def if the variable is unset or empty.
func envNum(name string, def uint64) (uint64, error) {
	raw, ok := os.LookupEnv(name)
	if !ok || raw == "" {
		return def, nil
	}
	n, err := strconv.ParseUint(raw, 10, 64)
	if err != nil {
		return 0, fmt.Errorf("%s=%q: not a whole number", name, raw)
	}
	return n, nil
}

// envStr reads a string from the environment, or returns def.
func envStr(name, def string) string {
	if raw, ok := os.LookupEnv(name); ok && raw != "" {
		return raw
	}
	return def
}

// fromEnv collects the defaults worldd will start with, refusing to
// start at all on a value it cannot read.
func fromEnv() (defaults, error) {
	d := defaults{log: envStr("WORLD_LOG", "world.jsonl")}
	var err error
	if d.seed, err = envNum("WORLD_SEED", 5); err != nil {
		return d, err
	}
	if d.ticks, err = envNum("WORLD_TICKS", 600); err != nil {
		return d, err
	}
	d.report, err = envNum("WORLD_REPORT", 0)
	return d, err
}

os.LookupEnv returns the value and a second result saying whether the variable was set at all, and that second result is what makes defaults possible: an unset WORLD_SEED is a different situation from WORLD_SEED=0, and seed zero is a perfectly good world somebody might want. strconv.ParseUint turns text into a number and, more usefully, reports when it cannot. fromEnv passes that failure up instead of falling back to 5, because a world that ran seed 5 after you fat-fingered the seed would be the worst available outcome: you would stare at a valley and draw conclusions about a number that never ran.

The other new value in that struct is report, and it exists because of where this program runs. Detached inside a container, worldd has no terminal and nobody sitting in front of it. Its standard output becomes the only sign of life anybody outside can see, and a program that prints two lines in ten minutes is indistinguishable from a program that hung. So the world gets a heartbeat: every N ticks, one line saying what it looks like now.

▣ Build · stage 2: flags on top of the environment, and a heartbeat
// cmd/worldd/main.go — the top of main, replacing last chapter's flag block
	d, err := fromEnv()
	if err != nil {
		fmt.Fprintln(os.Stderr, "worldd:", err)
		os.Exit(1)
	}

	seed := flag.Uint64("seed", d.seed, "the number that names this world")
	ticks := flag.Uint64("ticks", d.ticks, "how many ticks to take")
	logPath := flag.String("log", d.log, "where to write the event log")
	report := flag.Uint64("report", d.report, "print a status line every N ticks; 0 for silence")
	fast := flag.Bool("fast", false, "deliver ticks as fast as they compute")
	flag.Parse()

	cfg := sim.Config{Seed: *seed, Ticks: *ticks, Log: *logPath, Paced: !*fast}
	if *report > 0 {
		cfg.Watch = func(w *sim.World) {
			if w.Tick()%*report == 0 {
				fmt.Printf("tick %d: water %d cells, %d entities\n",
					w.Tick(), w.Ground.Count(sim.Water), w.Population())
			}
		}
	}
$ WORLD_SEED=5 WORLD_TICKS=200 WORLD_REPORT=50 WORLD_LOG=/tmp/h.jsonl go run ./cmd/worldd -fast
worldd 0.0.1 seed 5 ticks 200 -> /tmp/h.jsonl
tick 50: water 17 cells, 3 entities
tick 100: water 18 cells, 3 entities
tick 150: water 19 cells, 3 entities
tick 200: water 21 cells, 3 entities
worldd: the world stopped cleanly
$ WORLD_SEED=5 go run ./cmd/worldd -fast -seed 9 -ticks 20 -log /tmp/i.jsonl
worldd 0.0.1 seed 9 ticks 20 -> /tmp/i.jsonl
worldd: the world stopped cleanly
$ WORLD_TICKS=soon go run ./cmd/worldd
worldd: WORLD_TICKS="soon": not a whole number
exit status 1

Three runs show three behaviours. The first is configured entirely from the environment and it reads like any other invocation, because a flag default is still a flag default no matter where the number came from. The second sets WORLD_SEED=5 and then asks for seed 9 on the command line, and 9 wins: flag.Parse overwrites the default with whatever the argument list says, so precedence falls out of the ordering rather than out of any code you had to write. The third refuses before the log is opened, before the grid is generated, before a single tick runs. A server that is going to be wrong should be wrong immediately.

cfg.Watch is the first function value in this book: Config.Watch is a field of type func(*sim.World), which sim.Run calls after every successful tick when it is not nil. The closure assigned here captures report, a variable belonging to main, and keeps reading it long after main has moved on to waiting for a signal. So the simulation reports on itself while the sim package still contains no printing at all, exactly as chapter 2 promised. It does not print. It calls back to whoever asked to be told.

The container image

A container image is a filesystem plus a note saying which program to start inside it. You describe one in a Containerfile: a list of instructions, each taking the filesystem built so far and producing a new one. FROM picks the filesystem to begin with, COPY brings files in from your project, RUN executes a command inside the half-built image and keeps whatever it changed, ENV writes a default environment variable into the image, and ENTRYPOINT records the program to start when somebody runs a container from the finished result.

The obvious version begins FROM the official Go image, copies the source in, and builds. It works first time, and it ships a 900 MB image holding a complete Go compiler, the standard library source, a package cache and a Debian userland, in order to run a binary of about three megabytes. Everything in there is a thing that can have a security flaw, and none of it is needed once the build finishes. So the Containerfile describes two images and keeps only the second.

▣ Build · stage 3: the Containerfile
# Containerfile — build worldd, then ship only worldd.

# Stage 1: the whole Go toolchain, used once and then discarded.
FROM docker.io/library/golang:1.26 AS builder
WORKDIR /src
COPY go.mod ./
COPY cmd ./cmd
COPY internal ./internal
RUN CGO_ENABLED=0 go build -o /out/worldd ./cmd/worldd

# Stage 2: the image that actually ships.
FROM docker.io/library/alpine:3.22
COPY --from=builder /out/worldd /usr/local/bin/worldd
ENV WORLD_LOG=/data/world.jsonl
ENTRYPOINT ["worldd"]

The second FROM is what makes this a multi-stage build: it discards everything above it and starts again from a bare Alpine, the same eight-megabyte image you ran back in chapter 1. The only thing that survives the discard is what COPY --from=builder explicitly carries across, which here is one file. CGO_ENABLED=0 tells the toolchain to build with no C dependency at all, producing a binary that carries everything it needs and asks the host for no shared libraries; that is what lets a binary compiled against Debian's userland run unmodified on Alpine's very different one. ENV WORLD_LOG puts a default into the image itself, so the image knows where a world's history belongs and the person starting a container does not have to. And ENTRYPOINT in bracket form starts worldd as the container's first process with no shell in between, which matters a great deal when a signal arrives.

These FROM lines retain the original teaching tags. Both can move: golang:1.26 can gain a compiler patch and alpine:3.22 can gain updated packages. For an exact reference build, resolve both images with podman image inspect, record their RepoDigests, replace the tags with those digest references and select the reference platform. The original volume did not record those digests, so the build log below documents its capture rather than a permanent identity for either tag.

▣ Build · stage 4: build it, then weigh what came out
podman build -t worldd:0.0.1 .
$ podman build -t worldd:0.0.1 .   (blob-copying lines cut; the ids below are content hashes and yours will read differently)
[1/2] STEP 1/6: FROM docker.io/library/golang:1.26 AS builder
[1/2] STEP 2/6: WORKDIR /src
--> c2ad00eb14b0
[1/2] STEP 3/6: COPY go.mod ./
--> fb066eb79825
[1/2] STEP 4/6: COPY cmd ./cmd
--> daade6316d7c
[1/2] STEP 5/6: COPY internal ./internal
--> fa12bc59a867
[1/2] STEP 6/6: RUN CGO_ENABLED=0 go build -o /out/worldd ./cmd/worldd
--> c15e3e443d21
[2/2] STEP 1/4: FROM docker.io/library/alpine:3.22
[2/2] STEP 2/4: COPY --from=builder /out/worldd /usr/local/bin/worldd
--> fdec6d3d6206
[2/2] STEP 3/4: ENV WORLD_LOG=/data/world.jsonl
--> 84826267cd3a
[2/2] STEP 4/4: ENTRYPOINT ["worldd"]
[2/2] COMMIT worldd:0.0.1
--> adab9d6f6eae
Successfully tagged localhost/worldd:0.0.1
$ podman images --format "{{.Repository}}:{{.Tag}}  {{.Size}}" localhost/worldd
localhost/worldd:0.0.1  11.7 MB

The step numbering shows the whole design: [1/2] for the builder's six instructions, [2/2] for the four that make the image you keep. Every line ending in a hexadecimal id is a layer, a saved snapshot of the filesystem after that instruction, and those ids are content hashes, so yours will read differently while meaning the same thing. The shipped image weighs 11.7 MB against the builder's 900: Alpine's 8.6 plus three megabytes of worldd, and no compiler at all. Layers are also a cache, which is why the two COPY lines are separate instead of one COPY . .. Change a file under internal/ and podman re-runs from COPY internal downward; the steps above it, holding source you almost never edit, stay cached.

The podman volume

Start a container from that image and it gets a writable layer of its own: a scratch filesystem stacked on the image's read-only one, holding anything the process writes. It is genuinely writable, the log appends into it exactly as it appends to your disk, and it is deleted the moment the container is removed. For a world whose entire memory is one file, that arrangement is a trap.

A volume is podman's answer: storage that belongs to the machine instead of to any container, mounted into a container at a path you pick. Create one, mount it at /data, and the log the world writes lands somewhere no podman rm can reach.

▣ Build · stage 5: the world starts, and you walk away
podman volume create valley
podman run -d --name worldd \
  -e WORLD_SEED=5 -e WORLD_TICKS=200 -e WORLD_REPORT=50 \
  -v valley:/data \
  worldd:0.0.1
$ podman volume create valley
valley
$ podman run -d --name worldd -e WORLD_SEED=5 -e WORLD_TICKS=200 -e WORLD_REPORT=50 -v valley:/data worldd:0.0.1
c4b835fe418c85c85670a7ca51f7858ac9d538e1730cdd7748c65dc75846940c
$ podman ps --format "{{.Names}}  {{.Image}}  {{.Status}}"
worldd  localhost/worldd:0.0.1  Up Less than a second

-d is the chapter's whole argument in two characters: run detached. podman prints the container's id, hands your terminal straight back, and the world goes on ticking with no terminal attached to it. --name gives it something you can type instead of that id, each -e sets one environment variable inside the container, which is precisely where fromEnv looks, and -v valley:/data mounts the volume at the directory the image's own ENV already named.

Nothing is printing to your screen any more. The window into a detached container is podman logs, which replays everything the container's first process has written to standard output since it started. Run it twice, seconds apart, and watch the world accumulate.

▣ Build · stage 6: reading a world you are not attached to
podman logs worldd
podman ps -a --format "{{.Names}}  {{.Status}}"
$ podman logs worldd   (six seconds in)
worldd 0.0.1 seed 5 ticks 200 -> /data/world.jsonl
tick 50: water 17 cells, 3 entities
$ podman logs worldd   (twenty-three seconds in)
worldd 0.0.1 seed 5 ticks 200 -> /data/world.jsonl
tick 50: water 17 cells, 3 entities
tick 100: water 18 cells, 3 entities
tick 150: water 19 cells, 3 entities
tick 200: water 21 cells, 3 entities
worldd: the world stopped cleanly
$ podman ps -a --format "{{.Names}}  {{.Status}}"
worldd  Exited (0) 3 seconds ago

Two hundred paced ticks is twenty seconds of world, and the second reading catches it after the loop finished. The heartbeat lines are already earning their place: the pond went from 17 cells to 21 over those twenty seconds, and you know it without having watched a single tick. The last reading gains two squares instead of one because the spring seeps every forty ticks and tick 200 is itself a seeping tick, so that line reports the world one instant after its fifth seep. The exit status is podman reporting what the process returned, and 0 is the difference between a world that finished and a world that fell over. podman logs -f worldd follows the stream live, the way tail -f follows a file.

The container has exited, so remove it and see what is left behind. Your host cannot read the volume's files directly without help, but that is a small obstacle: mount the same volume into a throwaway Alpine container and use ordinary tools on it.

▣ Build · stage 7: the history outlives the container
podman rm worldd
podman run --rm -v valley:/data docker.io/library/alpine:3.22 \
  sh -c 'wc -l /data/world.jsonl; tail -1 /data/world.jsonl'
$ podman rm worldd
worldd
$ podman run --rm -v valley:/data docker.io/library/alpine:3.22 sh -c 'wc -l /data/world.jsonl; tail -1 /data/world.jsonl'
322 /data/world.jsonl
{"t":200,"ev":"stop","what":"ticks complete"}

The process that wrote those lines is gone, and so is the container it ran inside. The 322 lines are not. The container was the world's body for twenty seconds; the volume is where the world's memory lives regardless of which body wrote it. Any container mounting valley can read that file, including one running a program with no connection to worldd, which is exactly what the Alpine container just was.

⚠ Worked failure: the convenience that quietly ate a run

Forget the -v flag with the image as written and worldd refuses to start: open /data/world.jsonl: no such file or directory, because /data only exists when something is mounted there. The obvious tidy-up is to stop the image from being fussy. Add one line to the Containerfile so the directory is always present:

RUN mkdir /data
ENV WORLD_LOG=/data/world.jsonl

Now it starts anywhere, mounted or not, and the error is gone. Run it twice without a volume, copying the log out of each container afterward, and look at what the fix bought:

$ podman run --name ghost -e WORLD_TICKS=200 worldd:mkdir -fast; podman cp ghost:/data/world.jsonl /tmp/g1.jsonl; wc -l < /tmp/g1.jsonl
worldd 0.0.1 seed 5 ticks 200 -> /data/world.jsonl
worldd: the world stopped cleanly
322
$ podman rm ghost; podman run --name ghost -e WORLD_TICKS=200 worldd:mkdir -fast; podman cp ghost:/data/world.jsonl /tmp/g2.jsonl; wc -l < /tmp/g2.jsonl
ghost
worldd 0.0.1 seed 5 ticks 200 -> /data/world.jsonl
worldd: the world stopped cleanly
322

322, then 322. Against the volume those same two runs leave a file of 644 lines, because the second finds the first one's history waiting and appends to it. Reason from the symptom: the log is opened with O_APPEND and never truncates, so a count that fails to grow can only mean the second run opened a different file from the one the first wrote. Same path, different filesystem. Nothing in the Go code is wrong; twenty seconds of history went faithfully into storage that was scheduled for deletion, and podman rm carried out the sentence. The lesson is about which failure you would rather have. The unfixed image refuses to start when storage is missing; the mkdir makes it start happily and lose everything quietly. When state has nowhere durable to go, refusing to run is the useful behaviour.

One thing has to work before you can leave a world running for real: stopping it without hurting it. podman stop sends the container's first process a SIGTERM, the polite request to shut down, and waits before resorting to force. Last chapter's signal handler is already listening for exactly that. Give the container a tick count large enough that it will never reach it, and stop it by hand.

▣ Build · stage 8: a world with no end in sight, ended on purpose
podman run -d --name forever -e WORLD_TICKS=100000 -e WORLD_REPORT=50 \
  -e WORLD_LOG=/data/forever.jsonl -v valley:/data worldd:0.0.1
podman stop forever
podman logs forever
$ podman stop forever; podman logs forever   (stopped by hand after nine seconds; your run will get further or less far)
forever
worldd 0.0.1 seed 5 ticks 100000 -> /data/forever.jsonl
tick 50: water 17 cells, 3 entities
worldd: the world stopped cleanly
$ podman run --rm -v valley:/data docker.io/library/alpine:3.22 tail -1 /data/forever.jsonl
{"t":90,"ev":"stop","what":"stopped"}

The world stopped between two ticks, wrote its final line, and closed its log, because a signal from outside the container reached the goroutine holding the stop channel. Tick 90 is where the clock happened to be when the request landed, so your number will differ; that is the one figure on this page allowed to vary, and it varies honestly, since how long you waited before typing stop is no part of the simulation. The last line reads "stopped" instead of "ticks complete", so a reader months from now can tell an operator's decision from a run that finished on its own. The ENTRYPOINT bracket form is what makes any of it work: written as a plain string, the container's first process would be a shell, the shell would swallow the SIGTERM, and podman would wait ten seconds before killing the world mid-tick with a line half written.

Four separate lifetimes

The split this chapter enforces is not a container convention that containers invented. It is a division by how often each part changes, and containers only make the division impossible to ignore.

One running world, split into image, environment, container and volume Four stacked rows, joined by a vertical line on the left. The first row is the image worldd version 0.0.1, described as the code, rebuilt when the source changes. The second row is the environment variable WORLD SEED equals 5, described as which world this is, read once at startup. The third row is the container named worldd, described as the tick loop now, discarded on every stop. The fourth row is the volume named valley, holding the file data slash world dot jsonl, described as outliving all of the above. one running world, four separate lifetimes image worldd:0.0.1 the code, rebuilt when the source changes WORLD_SEED=5 which world this is, read once at startup container worldd the tick loop now, discarded on every stop volume valley /data/world.jsonl, outliving all of the above rebuild the top, replace the middle, back up the bottom
Figure 11.1: The four pieces of a hosted world, ordered by how long each is meant to last. A code change rebuilds the image. A restart replaces the container. The seed picks which world is running. The volume is the only part meant to be permanent, and the only part podman rm cannot touch.

Code changes when you write code, which often. Identity changes when you decide to run a different world, rarely and deliberately. History changes several times a second and must never be lost. Keep all three in one place and the fastest-moving part sets the terms for everything else: edit a file and you put the history at risk. Split them and each can be handled on its own schedule. Images get rebuilt and thrown away without a thought, since they hold nothing that cannot be rebuilt from source, and containers get destroyed and recreated just as freely. The volume you back up, because it is the one part of the running system nobody could reconstruct.

That last claim wants a test, not a promise, and you built the test in chapter 10: one seed produces one history, byte for byte. What is new is where the two runs happen. Run the world twice more from the same image, into two files of its own, and then let a third container be the judge.

▣ Build · stage 9: the same seed, in containers that never met
podman run --rm -e WORLD_SEED=5 -e WORLD_TICKS=200 -v valley:/data \
  worldd:0.0.1 -fast -log /data/again.jsonl
podman run --rm -e WORLD_SEED=5 -e WORLD_TICKS=200 -v valley:/data \
  worldd:0.0.1 -fast -log /data/again2.jsonl
podman run --rm -v valley:/data docker.io/library/alpine:3.22 \
  sh -c 'diff /data/again.jsonl /data/again2.jsonl; echo "diff exit: $?"'
$ podman run --rm -e WORLD_SEED=5 -e WORLD_TICKS=200 -v valley:/data worldd:0.0.1 -fast -log /data/again.jsonl
worldd 0.0.1 seed 5 ticks 200 -> /data/again.jsonl
worldd: the world stopped cleanly
$ podman run --rm -v valley:/data docker.io/library/alpine:3.22 sh -c 'diff /data/again.jsonl /data/again2.jsonl; echo "diff exit: $?"; wc -l /data/again.jsonl /data/again2.jsonl'
diff exit: 0
      322 /data/again.jsonl
      322 /data/again2.jsonl
      644 total
$ podman run --rm -v valley:/data docker.io/library/alpine:3.22 sh -c 'diff /data/again.jsonl /data/nine.jsonl | head -5'
--- /data/again.jsonl
+++ /data/nine.jsonl
@@ -1,322 +1,340 @@
-{"t":0,"ev":"start","what":"seed 5"}
+{"t":0,"ev":"start","what":"seed 9"}

A silent diff and an exit status of 0 mean the two files agree to the byte: two containers, started minutes apart, each generating its own terrain, spawning its own entities and taking its own two hundred ticks, wrote the same 322 lines in the same order. The flags arrive through the image command. Everything after the image name in a podman run command is appended to the image's ENTRYPOINT, so -fast -log /data/again.jsonl reaches flag.Parse as if you had typed it at a shell. A third run with WORLD_SEED=9 settles the other half: same image, same command, one number different, and the histories part company on line 1 and stay parted for 340 lines. The seed is the whole of a world's identity, and it now travels in an environment variable instead of a source file.

Checkpoint

What is in the box is small. Rock and water, three entities, a pond that swallows a square of soil now and then. What is true about it is not small. It advances on its own clock. It writes down everything that happens to it. It can be stopped and restarted without losing a line. Its entire past reproduces from one number. The box is small, but those four properties are already true of it.

✓ Checkpoint: what you can now do
  • Make the environment supply flag defaults, explain why a flag typed on the command line still wins, and hand sim a func(*sim.World) so the simulation reports on itself without printing anything.
  • Write a two-stage Containerfile, say what COPY --from carries across the discard, and explain what CGO_ENABLED=0 buys when the builder is Debian and the runtime is Alpine.
  • Read podman build output as a list of layers, predict which steps a given edit invalidates, and order COPY lines to keep the cache working.
  • Start a detached container with a volume and an environment, then interrogate a world you are not attached to with podman logs, podman ps -a and an exit status.
  • Explain the 322-then-322 result that exposes a missing volume, and argue why an image that refuses to start beats one that starts and loses the run.
  • Say what ENTRYPOINT ["worldd"] makes possible when podman stop arrives, and what a shell in that position would have broken.
⚡ Exercises: try first, then reveal
Exercise 1: two valleys at once. Create a second volume and start a second container from the same image with a different seed and a different name, both running at the same time. Read both logs. What does one image serving two live worlds tell you about what an image is?

podman volume create valley2, then podman run -d --name worldd2 -e WORLD_SEED=9 -e WORLD_TICKS=100000 -e WORLD_REPORT=50 -v valley2:/data worldd:0.0.1, and podman ps shows both up with different heartbeats. Two worlds, two histories, one image and one copy of the binary on disk. An image is read-only and shared, so twenty worlds cost twenty writable layers and twenty processes, never twenty copies of worldd. That is the point of sharing an image: one binary can serve more than one live world.

Exercise 2: take Alpine out. Copy the Containerfile, change the second FROM to FROM scratch, build it as worldd:scratch, and run it against a volume. Measure the image. Then try podman exec into it and explain what comes back.

It builds, runs and ticks identically: 3.08 MB against 11.7, because scratch is the absence of an image and a statically linked Go binary needs nothing else. The cost arrives the first time you want to look inside. podman exec worldd ls /data fails with executable file 'ls' not found in $PATH: no shell, nothing but your binary. That trade, the smallest possible attack surface against no way to poke around, is a real choice, and this book keeps Alpine because a world under construction is one you will want to open up.

Exercise 3: where the cache breaks. Rebuild with no changes and count the Using cache lines. Then add a comment to a file under internal/sim/ and rebuild again. Which steps re-run, and which one surprises you?

An unchanged rebuild caches every step, and so does a rebuild after merely touching a file, because COPY hashes contents and ignores timestamps. Adding a comment invalidates COPY internal ./internal and forces the RUN go build under it, exactly as you would predict. The surprise is the next line: COPY --from=builder still reports Using cache, because a comment changes no compiled instruction, the binary comes out byte-identical, and podman hashes the file being copied rather than the step that produced it. An identical binary makes an identical layer, so the image you ship is the image you already had.