|
| 1 | +# Building Docker Images |
| 2 | + |
| 3 | +Testcontainers for Rust supports building Docker images directly within your tests. This is useful when you need to test against custom-built images with specific configurations, or when your test requires a dynamically generated Dockerfile. |
| 4 | + |
| 5 | +## Building a Simple Image |
| 6 | + |
| 7 | +Use [`GenericBuildableImage`](https://docs.rs/testcontainers/latest/testcontainers/struct.GenericBuildableImage.html) to define an image that will be built from a Dockerfile: |
| 8 | + |
| 9 | +```rust |
| 10 | +use testcontainers::{ |
| 11 | + core::WaitFor, |
| 12 | + runners::AsyncRunner, |
| 13 | + GenericBuildableImage, |
| 14 | +}; |
| 15 | + |
| 16 | +#[tokio::test] |
| 17 | +async fn test_custom_image() -> Result<(), Box<dyn std::error::Error>> { |
| 18 | + let image = GenericBuildableImage::new("my-test-app", "latest") |
| 19 | + .with_dockerfile_string(r#" |
| 20 | + FROM alpine:latest |
| 21 | + COPY --chmod=0755 app.sh /usr/local/bin/app |
| 22 | + ENTRYPOINT ["/usr/local/bin/app"] |
| 23 | + "#) |
| 24 | + .with_data( |
| 25 | + r#"#!/bin/sh |
| 26 | +echo "Hello from custom image!" |
| 27 | +"#, |
| 28 | + "./app.sh", |
| 29 | + ) |
| 30 | + .build_image() |
| 31 | + .await?; |
| 32 | + |
| 33 | + let container = image |
| 34 | + .with_wait_for(WaitFor::message_on_stdout("Hello from custom image!")) |
| 35 | + .start() |
| 36 | + .await?; |
| 37 | + |
| 38 | + Ok(()) |
| 39 | +} |
| 40 | +``` |
| 41 | + |
| 42 | +## Adding Files to the Build Context |
| 43 | + |
| 44 | +You can add files from your filesystem or provide inline data: |
| 45 | + |
| 46 | +### From Filesystem |
| 47 | + |
| 48 | +```rust |
| 49 | +let image = GenericBuildableImage::new("my-app", "latest") |
| 50 | + .with_dockerfile("./path/to/Dockerfile") |
| 51 | + .with_file("./target/release/myapp", "./myapp") |
| 52 | + .build_image() |
| 53 | + .await?; |
| 54 | +``` |
| 55 | + |
| 56 | +### Inline Data |
| 57 | + |
| 58 | +```rust |
| 59 | +let image = GenericBuildableImage::new("my-app", "latest") |
| 60 | + .with_dockerfile_string("FROM alpine:latest\nCOPY config.json /config.json") |
| 61 | + .with_data(r#"{"port": 8080}"#, "./config.json") |
| 62 | + .build_image() |
| 63 | + .await?; |
| 64 | +``` |
| 65 | + |
| 66 | +## Build Options |
| 67 | + |
| 68 | +The [`BuildImageOptions`](https://docs.rs/testcontainers/latest/testcontainers/core/struct.BuildImageOptions.html) struct provides fine-grained control over the build process. |
| 69 | + |
| 70 | +### Skip Building if Image Exists |
| 71 | + |
| 72 | +When running tests repeatedly, you can skip rebuilding if the image already exists: |
| 73 | + |
| 74 | +```rust |
| 75 | +use testcontainers::core::BuildImageOptions; |
| 76 | + |
| 77 | +let image = GenericBuildableImage::new("my-app", "v1.0") |
| 78 | + .with_dockerfile_string("FROM alpine:latest") |
| 79 | + .build_image_with( |
| 80 | + BuildImageOptions::new() |
| 81 | + .with_skip_if_exists(true) |
| 82 | + ) |
| 83 | + .await?; |
| 84 | +``` |
| 85 | + |
| 86 | +This option: |
| 87 | +- Checks if an image with the same descriptor (name:tag) already exists |
| 88 | +- Skips the build if found, using the existing image |
| 89 | +- Is thread-safe - parallel tests building the same image will be serialized |
| 90 | + |
| 91 | +### Disable Build Cache |
| 92 | + |
| 93 | +Force a fresh build without using Docker's layer cache: |
| 94 | + |
| 95 | +```rust |
| 96 | +let image = GenericBuildableImage::new("my-app", "latest") |
| 97 | + .with_dockerfile_string("FROM alpine:latest\nRUN apk update") |
| 98 | + .build_image_with( |
| 99 | + BuildImageOptions::new() |
| 100 | + .with_no_cache(true) |
| 101 | + ) |
| 102 | + .await?; |
| 103 | +``` |
| 104 | + |
| 105 | +### Build Arguments |
| 106 | + |
| 107 | +Pass build-time variables to your Dockerfile using `ARG` instructions: |
| 108 | + |
| 109 | +```rust |
| 110 | +let image = GenericBuildableImage::new("my-app", "latest") |
| 111 | + .with_dockerfile_string(r#" |
| 112 | + FROM alpine:latest |
| 113 | + ARG VERSION |
| 114 | + ARG BUILD_DATE |
| 115 | + RUN echo "Building version ${VERSION} on ${BUILD_DATE}" |
| 116 | + "#) |
| 117 | + .build_image_with( |
| 118 | + BuildImageOptions::new() |
| 119 | + .with_build_arg("VERSION", "1.0.0") |
| 120 | + .with_build_arg("BUILD_DATE", "2024-10-25") |
| 121 | + ) |
| 122 | + .await?; |
| 123 | +``` |
| 124 | + |
| 125 | +You can also provide build arguments as a HashMap: |
| 126 | + |
| 127 | +```rust |
| 128 | +use std::collections::HashMap; |
| 129 | + |
| 130 | +let mut args = HashMap::new(); |
| 131 | +args.insert("VERSION".to_string(), "1.0.0".to_string()); |
| 132 | +args.insert("ENVIRONMENT".to_string(), "test".to_string()); |
| 133 | + |
| 134 | +let image = GenericBuildableImage::new("my-app", "latest") |
| 135 | + .with_dockerfile_string("FROM alpine:latest\nARG VERSION\nARG ENVIRONMENT") |
| 136 | + .build_image_with( |
| 137 | + BuildImageOptions::new() |
| 138 | + .with_build_args(args) |
| 139 | + ) |
| 140 | + .await?; |
| 141 | +``` |
| 142 | + |
| 143 | +### Combining Options |
| 144 | + |
| 145 | +All build options can be chained together: |
| 146 | + |
| 147 | +```rust |
| 148 | +let image = GenericBuildableImage::new("my-app", "latest") |
| 149 | + .with_dockerfile_string("FROM alpine:latest\nARG VERSION") |
| 150 | + .build_image_with( |
| 151 | + BuildImageOptions::new() |
| 152 | + .with_skip_if_exists(true) |
| 153 | + .with_no_cache(false) |
| 154 | + .with_build_arg("VERSION", "1.0.0") |
| 155 | + ) |
| 156 | + .await?; |
| 157 | +``` |
| 158 | + |
| 159 | +## Synchronous API |
| 160 | + |
| 161 | +For non-async tests, use the [`SyncBuilder`](https://docs.rs/testcontainers/latest/testcontainers/runners/trait.SyncBuilder.html) trait to build images and [`SyncRunner`](https://docs.rs/testcontainers/latest/testcontainers/runners/trait.SyncRunner.html) to run containers (requires the `blocking` feature): |
| 162 | + |
| 163 | +```rust |
| 164 | +use testcontainers::{ |
| 165 | + core::BuildImageOptions, |
| 166 | + runners::{SyncBuilder, SyncRunner}, |
| 167 | + GenericBuildableImage, |
| 168 | +}; |
| 169 | + |
| 170 | +#[test] |
| 171 | +fn test_sync_build() -> Result<(), Box<dyn std::error::Error>> { |
| 172 | + let image = GenericBuildableImage::new("my-app", "latest") |
| 173 | + .with_dockerfile_string("FROM alpine:latest") |
| 174 | + .build_image()?; |
| 175 | + |
| 176 | + let container = image.start()?; |
| 177 | + Ok(()) |
| 178 | +} |
| 179 | + |
| 180 | +#[test] |
| 181 | +fn test_sync_build_with_options() -> Result<(), Box<dyn std::error::Error>> { |
| 182 | + let image = GenericBuildableImage::new("my-app", "latest") |
| 183 | + .with_dockerfile_string("FROM alpine:latest\nARG VERSION") |
| 184 | + .build_image_with( |
| 185 | + BuildImageOptions::new() |
| 186 | + .with_skip_if_exists(true) |
| 187 | + .with_build_arg("VERSION", "1.0.0") |
| 188 | + )?; |
| 189 | + |
| 190 | + let container = image.start()?; |
| 191 | + Ok(()) |
| 192 | +} |
| 193 | +``` |
| 194 | + |
| 195 | +## Best Practices |
| 196 | + |
| 197 | +### Use Descriptive Tags |
| 198 | + |
| 199 | +Use meaningful image names and tags to avoid conflicts: |
| 200 | + |
| 201 | +```rust |
| 202 | +// Good: specific and descriptive |
| 203 | +GenericBuildableImage::new("test-user-service", "integration-v1") |
| 204 | + |
| 205 | +// Avoid: generic names that might conflict |
| 206 | +GenericBuildableImage::new("test", "latest") |
| 207 | +``` |
| 208 | + |
| 209 | +### Leverage skip_if_exists for Faster Tests |
| 210 | + |
| 211 | +When your image doesn't change between test runs: |
| 212 | + |
| 213 | +```rust |
| 214 | +let image = GenericBuildableImage::new("my-stable-app", "v1.0") |
| 215 | + .with_dockerfile_string("FROM alpine:latest\nRUN apk add curl") |
| 216 | + .build_image_with( |
| 217 | + BuildImageOptions::new() |
| 218 | + .with_skip_if_exists(true) |
| 219 | + ) |
| 220 | + .await?; |
| 221 | +``` |
| 222 | + |
| 223 | +### Use Build Arguments for Flexibility |
| 224 | + |
| 225 | +Make your test images configurable: |
| 226 | + |
| 227 | +```rust |
| 228 | +fn build_test_image(version: &str) -> GenericBuildableImage { |
| 229 | + GenericBuildableImage::new("my-app", version) |
| 230 | + .with_dockerfile_string("FROM alpine:latest\nARG APP_VERSION\nENV VERSION=$APP_VERSION") |
| 231 | + .build_image_with( |
| 232 | + BuildImageOptions::new() |
| 233 | + .with_build_arg("APP_VERSION", version) |
| 234 | + ) |
| 235 | +} |
| 236 | +``` |
| 237 | + |
| 238 | +## Common Patterns |
| 239 | + |
| 240 | +### Building from a Complex Dockerfile |
| 241 | + |
| 242 | +```rust |
| 243 | +let dockerfile = r#" |
| 244 | +FROM rust:1.75 as builder |
| 245 | +WORKDIR /app |
| 246 | +COPY Cargo.toml Cargo.lock ./ |
| 247 | +COPY src ./src |
| 248 | +RUN cargo build --release |
| 249 | +
|
| 250 | +FROM debian:bookworm-slim |
| 251 | +COPY --from=builder /app/target/release/myapp /usr/local/bin/ |
| 252 | +CMD ["/usr/local/bin/myapp"] |
| 253 | +"#; |
| 254 | + |
| 255 | +let image = GenericBuildableImage::new("my-rust-app", "latest") |
| 256 | + .with_dockerfile_string(dockerfile) |
| 257 | + .with_file("./Cargo.toml", "./Cargo.toml") |
| 258 | + .with_file("./Cargo.lock", "./Cargo.lock") |
| 259 | + .with_file("./src", "./src") |
| 260 | + .build_image() |
| 261 | + .await?; |
| 262 | +``` |
| 263 | + |
| 264 | +### Building Multiple Variants |
| 265 | + |
| 266 | +```rust |
| 267 | +async fn build_variant(variant: &str, port: u16) -> Result<GenericImage, Box<dyn std::error::Error>> { |
| 268 | + GenericBuildableImage::new("my-app", variant) |
| 269 | + .with_dockerfile_string(format!(r#" |
| 270 | + FROM alpine:latest |
| 271 | + ARG PORT |
| 272 | + ENV APP_PORT=$PORT |
| 273 | + CMD ["sh", "-c", "echo 'Running on port $APP_PORT' && sleep infinity"] |
| 274 | + "#)) |
| 275 | + .build_image_with( |
| 276 | + BuildImageOptions::new() |
| 277 | + .with_build_arg("PORT", port.to_string()) |
| 278 | + .with_skip_if_exists(true) |
| 279 | + ) |
| 280 | + .await |
| 281 | +} |
| 282 | + |
| 283 | +#[tokio::test] |
| 284 | +async fn test_multiple_variants() -> Result<(), Box<dyn std::error::Error>> { |
| 285 | + let image1 = build_variant("dev", 8080).await?; |
| 286 | + let image2 = build_variant("staging", 8081).await?; |
| 287 | + |
| 288 | + // Use images in tests... |
| 289 | + Ok(()) |
| 290 | +} |
| 291 | +``` |
| 292 | + |
| 293 | +## Troubleshooting |
| 294 | + |
| 295 | +### Build Fails with "no active session" Error |
| 296 | + |
| 297 | +This typically occurs when BuildKit encounters issues. Potential solutions: |
| 298 | + |
| 299 | +1. Remove the build cache and retry: |
| 300 | + ```rust |
| 301 | + BuildImageOptions::new().with_no_cache(true) |
| 302 | + ``` |
| 303 | + |
| 304 | +2. Ensure Docker daemon is running properly: |
| 305 | + ```bash |
| 306 | + docker info |
| 307 | + ``` |
| 308 | + |
| 309 | +3. In CI environments, ensure BuildKit is properly initialized |
| 310 | + |
| 311 | +### Image Not Found After Build |
| 312 | + |
| 313 | +Verify the descriptor matches exactly: |
| 314 | + |
| 315 | +```rust |
| 316 | +let image = GenericBuildableImage::new("my-app", "v1.0") // Name and tag must match |
| 317 | + .with_dockerfile_string("FROM alpine:latest") |
| 318 | + .build_image() |
| 319 | + .await?; |
| 320 | + |
| 321 | +// The image is now available as "my-app:v1.0" |
| 322 | +``` |
| 323 | + |
| 324 | +### Build Arguments Not Working |
| 325 | + |
| 326 | +Ensure ARG instructions are in the Dockerfile before they're used: |
| 327 | + |
| 328 | +```dockerfile |
| 329 | +# Correct |
| 330 | +ARG VERSION |
| 331 | +ENV APP_VERSION=$VERSION |
| 332 | + |
| 333 | +# Incorrect - ARG comes after usage |
| 334 | +ENV APP_VERSION=$VERSION |
| 335 | +ARG VERSION |
| 336 | +``` |
0 commit comments